siluzan-tso-cli 1.1.60-beta.2 → 1.1.60-beta.3

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +882 -253
  3. package/dist/skill/_meta.json +2 -2
  4. package/dist/skill/references/core/agent-conventions.md +10 -0
  5. package/dist/skill/references/core/playbooks.md +12 -11
  6. package/dist/skill/references/core/workflows.md +1 -1
  7. package/dist/skill/references/report-templates/README.md +2 -0
  8. package/dist/skill/references/report-templates/bing-period-report.md +2 -0
  9. package/dist/skill/references/report-templates/google-ads-diagnosis.md +2 -0
  10. package/dist/skill/references/report-templates/google-inquiry-analysis.md +11 -0
  11. package/dist/skill/references/report-templates/google-period-report-excel.md +2 -0
  12. package/dist/skill/references/report-templates/google-period-report.md +4 -1
  13. package/dist/skill/references/report-templates/google-quarterly-report.md +2 -0
  14. package/dist/skill/references/report-templates/market-analysis-report.md +2 -0
  15. package/dist/skill/references/report-templates/meta-account-diagnosis-report.md +2 -0
  16. package/dist/skill/references/report-templates/meta-period-report.md +2 -0
  17. package/dist/skill/references/report-templates/okki-weekly-google-client.md +21 -0
  18. package/dist/skill/references/report-templates/tiktok-period-report.md +2 -0
  19. package/dist/skill/references/report-templates/website-diagnosis-report.md +2 -0
  20. package/dist/skill/references/report-templates/yandex-period-report.md +2 -0
  21. package/dist/skill/report-templates/README.md +2 -0
  22. package/dist/skill/report-templates/bing-period-report.html +16 -3
  23. package/dist/skill/report-templates/bing-period-report.md +2 -0
  24. package/dist/skill/report-templates/google-ads-diagnosis.md +2 -0
  25. package/dist/skill/report-templates/google-inquiry-analysis.md +11 -0
  26. package/dist/skill/report-templates/google-period-report-excel.md +2 -0
  27. package/dist/skill/report-templates/google-period-report.html +35 -6
  28. package/dist/skill/report-templates/google-period-report.md +4 -1
  29. package/dist/skill/report-templates/google-quarterly-report.html +1 -1
  30. package/dist/skill/report-templates/google-quarterly-report.md +2 -0
  31. package/dist/skill/report-templates/market-analysis-report.md +2 -0
  32. package/dist/skill/report-templates/meta-account-diagnosis-report.md +2 -0
  33. package/dist/skill/report-templates/meta-period-report.md +2 -0
  34. package/dist/skill/report-templates/okki-weekly-google-client.md +21 -0
  35. package/dist/skill/report-templates/tiktok-period-report.html +4 -2
  36. package/dist/skill/report-templates/tiktok-period-report.md +2 -0
  37. package/dist/skill/report-templates/website-diagnosis-report.md +2 -0
  38. package/dist/skill/report-templates/yandex-period-report.html +4 -2
  39. package/dist/skill/report-templates/yandex-period-report.md +2 -0
  40. package/dist/skill/scripts/install.ps1 +1 -1
  41. package/dist/skill/scripts/install.sh +1 -1
  42. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5212,6 +5212,63 @@ var init_media_account_batch = __esm({
5212
5212
  }
5213
5213
  });
5214
5214
 
5215
+ // src/report-i18n/lang.ts
5216
+ function resolveReportLang(opts) {
5217
+ const flag = opts.flag?.trim();
5218
+ if (flag) return parseLang(flag, "--lang");
5219
+ if (opts.meta && typeof opts.meta === "object" && !Array.isArray(opts.meta)) {
5220
+ const raw = opts.meta.lang;
5221
+ if (raw == null || raw === "") return "zh";
5222
+ return parseLang(String(raw), "meta.lang");
5223
+ }
5224
+ return "zh";
5225
+ }
5226
+ function htmlLangAttr(lang) {
5227
+ return lang === "en" ? "en" : "zh-CN";
5228
+ }
5229
+ function stampMetaLang(payload, lang) {
5230
+ const prev = payload.meta;
5231
+ const meta = prev && typeof prev === "object" && !Array.isArray(prev) ? { ...prev, lang } : { lang };
5232
+ return { ...payload, meta };
5233
+ }
5234
+ function readReportLangOrExit(opts) {
5235
+ try {
5236
+ return resolveReportLang(opts);
5237
+ } catch (err) {
5238
+ const message = err instanceof Error ? err.message : String(err);
5239
+ console.error(`
5240
+ \u274C ${message}
5241
+ `);
5242
+ process.exit(1);
5243
+ }
5244
+ }
5245
+ function assertHtmlLangMatches(html, lang) {
5246
+ const match = html.match(/<html\b[^>]*\blang\s*=\s*["']([^"']+)["']/i);
5247
+ const found = (match?.[1] ?? "").trim().toLowerCase();
5248
+ const ok = lang === "en" ? found === "en" || found.startsWith("en-") : found === "zh" || found.startsWith("zh-");
5249
+ if (!ok) {
5250
+ throw new ReportLangError(
5251
+ `html lang="${found || "(missing)"}" \u4E0E\u7EC8\u7A3F\u8BED\u8A00 ${lang} \u4E0D\u4E00\u81F4\uFF08\u4E2D\u6587\u5199 zh-CN\uFF0C\u82F1\u6587\u5199 en\uFF09`
5252
+ );
5253
+ }
5254
+ }
5255
+ function parseLang(raw, source) {
5256
+ if (raw === "zh" || raw === "en") return raw;
5257
+ throw new ReportLangError(`${source} \u53EA\u63A5\u53D7 zh \u6216 en\uFF0C\u6536\u5230 ${raw}`);
5258
+ }
5259
+ var ReportLangError;
5260
+ var init_lang = __esm({
5261
+ "src/report-i18n/lang.ts"() {
5262
+ "use strict";
5263
+ ReportLangError = class extends Error {
5264
+ constructor(message) {
5265
+ super(message);
5266
+ this.name = "ReportLangError";
5267
+ }
5268
+ };
5269
+ }
5270
+ });
5271
+
5215
5272
  // src/commands/accounts-digest/list-fetch.ts
5216
5273
  async function fetchAccountsByMedia(media, config, opts) {
5217
5274
  const cfg = DIGEST_PLATFORM_CONFIG[media];
@@ -5317,11 +5374,12 @@ function escapeMdCell(s) {
5317
5374
  function accountLabel(row) {
5318
5375
  return row.advertiserName || row.name || "-";
5319
5376
  }
5320
- function buildHitTable(rows, maxCpa) {
5321
- const header = "| \u8D26\u6237\u540D\u79F0 | \u8D26\u6237ID | \u533A\u95F4\u6D88\u8017 | \u8F6C\u5316\u6B21\u6570 | \u8F6C\u5316\u6210\u672C | \u5E01\u79CD | \u547D\u4E2D\u539F\u56E0 |\n| --- | --- | ---: | ---: | ---: | --- | --- |";
5377
+ function buildHitTable(rows, maxCpa, lang = "zh") {
5378
+ const en = lang === "en";
5379
+ const header = en ? "| Account | Account ID | Spend | Conversions | CPA | Currency | Reason |\n| --- | --- | ---: | ---: | ---: | --- | --- |" : "| \u8D26\u6237\u540D\u79F0 | \u8D26\u6237ID | \u533A\u95F4\u6D88\u8017 | \u8F6C\u5316\u6B21\u6570 | \u8F6C\u5316\u6210\u672C | \u5E01\u79CD | \u547D\u4E2D\u539F\u56E0 |\n| --- | --- | ---: | ---: | ---: | --- | --- |";
5322
5380
  const lines = rows.map((r) => {
5323
- const reason = classifyDigestHit(r, maxCpa) === "zeroConversions" ? "\u96F6\u8F6C\u5316\u4E14\u6D88\u8017\u5DF2\u8D85\u9608\u503C" : "CPA\u9AD8\u4E8E\u9608\u503C";
5324
- const conv = r.conversions == null ? "\u8F6C\u5316\u672A\u8FD4\u56DE" : String(r.conversions);
5381
+ const reason = classifyDigestHit(r, maxCpa) === "zeroConversions" ? en ? "Zero conversions and spend over the threshold" : "\u96F6\u8F6C\u5316\u4E14\u6D88\u8017\u5DF2\u8D85\u9608\u503C" : en ? "CPA above the threshold" : "CPA\u9AD8\u4E8E\u9608\u503C";
5382
+ const conv = r.conversions == null ? en ? "Conversions missing" : "\u8F6C\u5316\u672A\u8FD4\u56DE" : String(r.conversions);
5325
5383
  const cpa = r.cpa == null ? "\u2014" : String(r.cpa);
5326
5384
  const spend = r.spend == null ? "\u2014" : String(r.spend);
5327
5385
  return `| ${escapeMdCell(accountLabel(r))} | ${r.mediaCustomerId} | ${spend} | ${conv} | ${cpa} | ${r.currencyCode ?? "\u2014"} | ${reason} |`;
@@ -5330,6 +5388,7 @@ function buildHitTable(rows, maxCpa) {
5330
5388
  }
5331
5389
  function buildDigestAgentBrief(opts) {
5332
5390
  const { rows, scanned, validAccountCount, spendPositiveCount, maxCpa, zeroConversions } = opts;
5391
+ const en = opts.lang === "en";
5333
5392
  let highCpa = 0;
5334
5393
  let zeroConvHits = 0;
5335
5394
  for (const r of rows) {
@@ -5343,11 +5402,11 @@ function buildDigestAgentBrief(opts) {
5343
5402
  const allClear = patrol && rows.length === 0;
5344
5403
  let userDelivery;
5345
5404
  if (allClear && maxCpa != null) {
5346
- userDelivery = `\u4ECA\u65E5\u6240\u6709\u8D26\u6237\u8F6C\u5316\u6210\u672C\u6B63\u5E38\uFF0C\u5747\u4F4E\u4E8E${maxCpa}\uFF08\u5404\u8D26\u6237\u6309\u81EA\u8EAB currencyCode \u6BD4\u8F83\uFF0C\u52FF\u8DE8\u5E01\u79CD\u5F53\u540C\u4E00\u5355\u4F4D\uFF09`;
5405
+ userDelivery = en ? `Every account's CPA is under ${maxCpa} today (compare each account in its own currencyCode)` : `\u4ECA\u65E5\u6240\u6709\u8D26\u6237\u8F6C\u5316\u6210\u672C\u6B63\u5E38\uFF0C\u5747\u4F4E\u4E8E${maxCpa}\uFF08\u5404\u8D26\u6237\u6309\u81EA\u8EAB currencyCode \u6BD4\u8F83\uFF0C\u52FF\u8DE8\u5E01\u79CD\u5F53\u540C\u4E00\u5355\u4F4D\uFF09`;
5347
5406
  } else if (allClear && zeroConversions) {
5348
- userDelivery = "\u533A\u95F4\u5185\u6CA1\u6709\u6D88\u8017>0\u4E14\u96F6\u8F6C\u5316\uFF08\u6216\u8F6C\u5316\u672A\u8FD4\u56DE\uFF09\u7684\u8D26\u6237";
5407
+ userDelivery = en ? "No account in this range has spend above 0 and zero conversions (or missing conversions)" : "\u533A\u95F4\u5185\u6CA1\u6709\u6D88\u8017>0\u4E14\u96F6\u8F6C\u5316\uFF08\u6216\u8F6C\u5316\u672A\u8FD4\u56DE\uFF09\u7684\u8D26\u6237";
5349
5408
  } else if (patrol && rows.length > 0) {
5350
- userDelivery = buildHitTable(rows, maxCpa);
5409
+ userDelivery = buildHitTable(rows, maxCpa, opts.lang ?? "zh");
5351
5410
  } else {
5352
5411
  userDelivery = "\u672A\u5F00 --max-cpa / --zero-conversions\u3002\u6309 data.items \u51FA\u8868\uFF1Bmeta.totals \u53EA\u5408\u8BA1 returned \u884C\uFF0C\u7981\u6B62\u5199\u6210\u5168\u90E8\u8D26\u6237\u3002";
5353
5412
  }
@@ -6114,7 +6173,8 @@ async function runAccountsDigest(opts) {
6114
6173
  validAccountCount: validIds.length,
6115
6174
  spendPositiveCount,
6116
6175
  maxCpa: opts.maxCpa,
6117
- zeroConversions: !!opts.zeroConversions
6176
+ zeroConversions: !!opts.zeroConversions,
6177
+ lang: readReportLangOrExit({ flag: opts.lang })
6118
6178
  });
6119
6179
  const digestPayload = { ok: true, data: { items: rows }, meta, agentBrief };
6120
6180
  if (await emitCliJsonOrSnapshot(opts, {
@@ -6201,6 +6261,7 @@ var init_run = __esm({
6201
6261
  init_list_fetch();
6202
6262
  init_login_customer_id();
6203
6263
  init_brief();
6264
+ init_lang();
6204
6265
  init_campaign_types();
6205
6266
  init_shared();
6206
6267
  init_enabled_campaigns();
@@ -6248,7 +6309,7 @@ function register10(program2) {
6248
6309
  "--json-out <path>",
6249
6310
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
6250
6311
  void 0
6251
- ).option("--refresh-dp", "\u5F3A\u5236\u91CD\u62C9 Datapermission \u540E\u518D\u8BF7\u6C42", false).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
6312
+ ).option("--refresh-dp", "\u5F3A\u5236\u91CD\u62C9 Datapermission \u540E\u518D\u8BF7\u6C42", false).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).option("--lang <lang>", "\u4EA4\u7ED9\u7528\u6237\u7684\u53E5\u5B50\u8BED\u8A00\uFF1Azh \u6216 en\uFF0C\u9ED8\u8BA4 zh").action(
6252
6313
  async (opts) => {
6253
6314
  await runAccountsDigest({
6254
6315
  token: opts.token,
@@ -6269,7 +6330,8 @@ function register10(program2) {
6269
6330
  withCampaignTypes: opts.withCampaignTypes,
6270
6331
  campaignTypeConcurrency: opts.campaignTypeConcurrency,
6271
6332
  withEnabledCampaigns: opts.withEnabledCampaigns,
6272
- enabledCampaignConcurrency: opts.enabledCampaignConcurrency
6333
+ enabledCampaignConcurrency: opts.enabledCampaignConcurrency,
6334
+ lang: opts.lang
6273
6335
  });
6274
6336
  }
6275
6337
  );
@@ -7229,6 +7291,372 @@ var init_xlsx_style_engine = __esm({
7229
7291
  }
7230
7292
  });
7231
7293
 
7294
+ // src/report-i18n/labels.ts
7295
+ function kpiLabels(lang) {
7296
+ return { lang, ...lang === "en" ? EN : ZH };
7297
+ }
7298
+ var ZH, EN;
7299
+ var init_labels = __esm({
7300
+ "src/report-i18n/labels.ts"() {
7301
+ "use strict";
7302
+ ZH = {
7303
+ cost: "\u6D88\u8017",
7304
+ impressions: "\u5C55\u793A\u6B21\u6570",
7305
+ clicks: "\u70B9\u51FB\u6B21\u6570",
7306
+ conversions: "\u8F6C\u5316\u6B21\u6570",
7307
+ ctr: "\u70B9\u51FB\u7387 CTR",
7308
+ cpc: "\u5E73\u5747\u70B9\u51FB\u8D39\u7528",
7309
+ cpa: "\u6BCF\u6B21\u8F6C\u5316\u8D39\u7528 CPA",
7310
+ analysis: "\u5206\u6790",
7311
+ suggestion: "\u5EFA\u8BAE",
7312
+ account: "\u8D26\u6237",
7313
+ currency: "\u8D27\u5E01",
7314
+ mom: "\u73AF\u6BD4"
7315
+ };
7316
+ EN = {
7317
+ cost: "Spend",
7318
+ impressions: "Impressions",
7319
+ clicks: "Clicks",
7320
+ conversions: "Conversions",
7321
+ ctr: "CTR",
7322
+ cpc: "Avg. CPC",
7323
+ cpa: "CPA",
7324
+ analysis: "Analysis",
7325
+ suggestion: "Recommendation",
7326
+ account: "Account",
7327
+ currency: "Currency",
7328
+ mom: "vs prior"
7329
+ };
7330
+ }
7331
+ });
7332
+
7333
+ // src/report-i18n/phrases.en.ts
7334
+ var ENGLISH_PHRASES;
7335
+ var init_phrases_en = __esm({
7336
+ "src/report-i18n/phrases.en.ts"() {
7337
+ "use strict";
7338
+ ENGLISH_PHRASES = {
7339
+ "Google Ads \u5468\u671F\u5206\u6790\u62A5\u544A": "Google Ads period report",
7340
+ "Google Ads \u8D26\u6237\u8BCA\u65AD\u62A5\u544A": "Google Ads account diagnosis",
7341
+ "Google \u5B63\u5EA6\u7ECF\u8425\u62A5\u544A": "Google quarterly business report",
7342
+ "Google \u6708\u5EA6\u7ECF\u8425\u62A5\u544A": "Google monthly business report",
7343
+ "Google \u7ECF\u8425\u62A5\u544A": "Google business report",
7344
+ "Bing \u5E7F\u544A\u8D26\u6237\u5206\u6790\u62A5\u544A": "Bing Ads account report",
7345
+ "Yandex Ads \u8D26\u6237\u5206\u6790\u62A5\u544A": "Yandex Ads account report",
7346
+ "TikTok \u5E7F\u544A\u4E3B\u8D26\u6237\u5206\u6790\u62A5\u544A": "TikTok Ads account report",
7347
+ "Meta / Facebook \u5468\u671F\u5206\u6790\u62A5\u544A": "Meta / Facebook period report",
7348
+ "Meta\uFF08Facebook\uFF09\u8D26\u6237\u8BCA\u65AD\u62A5\u544A": "Meta (Facebook) account diagnosis",
7349
+ "\u7F51\u7AD9\u8BCA\u65AD\u62A5\u544A": "Website diagnosis report",
7350
+ "\u5DE6\u53F3\u6ED1\u52A8\u67E5\u770B\u8868\u683C": "Swipe sideways to see the table",
7351
+ "\u5DE6\u53F3\u6ED1\u52A8\u67E5\u770B\u5176\u4F59\u5217": "Swipe sideways to see more columns",
7352
+ "\u7EDF\u8BA1\u533A\u95F4\u672A\u63D0\u4F9B": "Date range not provided",
7353
+ "\u672A\u627E\u5230\u62A5\u544A\u6570\u636E": "Report data was not found",
7354
+ "\u672A\u6CE8\u5165\u62A5\u544A\u6570\u636E\uFF0C\u8BF7\u901A\u8FC7 facebook-analysis render \u751F\u6210\u3002": "Report data was not injected. Generate this file with facebook-analysis render.",
7355
+ "\uFF08\u5F85 Agent \u64B0\u5199\u6267\u884C\u6458\u8981\uFF09": "(Executive summary not written yet)",
7356
+ "\uFF08\u5F85 Agent \u64B0\u5199\u4F18\u5316\u5EFA\u8BAE\uFF09": "(Recommendations not written yet)",
7357
+ "\uFF08\u5F85 Agent \u64B0\u5199\u884C\u52A8\u9879\uFF09": "(Action items not written yet)",
7358
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1A\u672C\u8282\u6682\u65E0\u5206\u6790": "Unavailable: no analysis for this section",
7359
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Adaily-metrics \u672A\u62C9\u53D6\u6216\u533A\u95F4\u5185\u65E0\u6570\u636E": "Unavailable: daily-metrics was not pulled or the range is empty",
7360
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Adimension-summary \u672A\u62C9\u53D6": "Unavailable: dimension-summary was not pulled",
7361
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Acampaigns \u672A\u62C9\u53D6": "Unavailable: campaigns was not pulled",
7362
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Adevices \u672A\u62C9\u53D6": "Unavailable: devices was not pulled",
7363
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Ageographic \u672A\u62C9\u53D6": "Unavailable: geographic was not pulled",
7364
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Akeywords \u672A\u62C9\u53D6": "Unavailable: keywords was not pulled",
7365
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Anegative-keywords \u672A\u62C9\u53D6": "Unavailable: negative-keywords was not pulled",
7366
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Aaudience \u672A\u62C9\u53D6\u6216\u672C\u671F\u95F4\u65E0\u53D7\u4F17\u884C": "Unavailable: audience was not pulled or this period has no audience rows",
7367
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Acreative \u672A\u62C9\u53D6": "Unavailable: creative was not pulled",
7368
+ "\u6570\u636E\u4E0D\u53EF\u7528\uFF1Amaterial \u672A\u62C9\u53D6\uFF08\u6807\u51C6\u8D26\u6237\u65E0 Dynamic Creative \u65F6\u5C5E\u6B63\u5E38\uFF09": "Unavailable: material was not pulled (normal when a standard account has no Dynamic Creative)",
7369
+ "\u5F53\u524D\u8D26\u6237\u5426\u8BCD 0 \u6761": "This account has 0 negative keywords",
7370
+ "\u672A\u8FBE\u5230\u8BE5\u9879\u9EC4\u91D1\u8D26\u6237\u6807\u51C6": "Does not meet this Gold Account criterion",
7371
+ "\u5DF2\u8FBE\u6807\uFF0C\u7EE7\u7EED\u4FDD\u6301": "Met \u2014 keep it up",
7372
+ "\u81EA\u9002\u5E94\u641C\u7D22\u5E7F\u544A": "Responsive search ads",
7373
+ "\u81EA\u9002\u5E94\u5C55\u793A\u5E7F\u544A": "Responsive display ads",
7374
+ "\u8F6C\u5316\u8FFD\u8E2A\u8BBE\u7F6E": "Conversion tracking",
7375
+ "G A\u4E0E\u5E7F\u544A\u8D26\u6237\u8054\u7ED3": "GA linked to the ads account",
7376
+ "\u9644\u52A0\u7ED3\u6784\u5316\u4FE1\u606F\u6458\u8981": "Structured snippets",
7377
+ "\u9644\u52A0\u94FE\u63A5": "Sitelinks",
7378
+ "\u9644\u52A0\u5BA3\u4F20\u4FE1\u606F": "Callouts",
7379
+ "\u9644\u52A0\u7535\u8BDD\u4FE1\u606F": "Call extensions",
7380
+ "\u53D7\u4F17\u7FA4\u4F53\u8BBE\u7F6E": "Audiences",
7381
+ "\u51FA\u4EF7\u7B56\u7565": "Bidding strategy",
7382
+ "\u8D26\u6237\u57FA\u672C\u4FE1\u606F\u4E0E\u76EE\u6807\u8BBE\u5B9A": "Account basics and goals",
7383
+ "\u8D26\u6237\u8BCA\u65AD\u6982\u89C8": "Diagnosis overview",
7384
+ "\u6838\u5FC3\u4E1A\u7EE9\u6307\u6807\u5FEB\u7167": "KPI snapshot",
7385
+ "\u8D26\u6237\u5065\u5EB7\u5EA6\u4E0E\u7ED3\u6784\u5206\u6790": "Account health and structure",
7386
+ "\u6295\u653E\u9884\u7B97\u4E0E\u7ADE\u4E89\u529B\u5206\u6790": "Budget and competitiveness",
7387
+ "\u76EE\u6807\u53D7\u4F17\u4E0E\u6295\u653E\u7B56\u7565": "Audience and targeting",
7388
+ "\u7740\u9646\u9875\u5206\u6790": "Landing pages",
7389
+ "\u5173\u952E\u8BCD\u4E0E\u641C\u7D22\u8BCD\u6D1E\u5BDF": "Keywords and search terms",
7390
+ "\u9884\u7B97\u4E0E\u51FA\u4EF7\u7B56\u7565": "Budget and bidding",
7391
+ "\u5E7F\u544A\u521B\u610F\u4E0E\u7D20\u6750\u4F18\u5316": "Ads and assets",
7392
+ "\u65B0\u4EA7\u54C1\u5E94\u7528": "New products",
7393
+ "\u8BCA\u65AD\u603B\u7ED3": "Diagnosis summary",
7394
+ "\u8BCA\u65AD\u9879": "Diagnosis sections",
7395
+ "\u8BCA\u65AD\u5468\u671F": "Diagnosis period",
7396
+ "\u6267\u884C\u6458\u8981": "Executive summary",
7397
+ "\u6267\u884C\u6458\u8981\uFF08\u603B\u89C8\uFF09": "Executive summary",
7398
+ "\u6BCF\u65E5\u6295\u653E\u8D8B\u52BF": "Daily trend",
7399
+ "\u5468\u671F\u6C47\u603B": "Period totals",
7400
+ "\u5E7F\u544A\u7CFB\u5217\u8868\u73B0": "Campaign performance",
7401
+ "\u8BBE\u5907\u5206\u5E03": "Devices",
7402
+ "\u5730\u57DF\u5206\u5E03\uFF08Top 20\uFF09": "Locations (top 20)",
7403
+ "\u5730\u57DF\u5206\u5E03": "Locations",
7404
+ "\u53D7\u4F17\u5206\u5E03\uFF08Top 20\uFF09": "Audiences (top 20)",
7405
+ "\u53D7\u4F17\u5206\u5E03": "Audiences",
7406
+ "\u5173\u952E\u8BCD\u8868\u73B0\uFF08Top 50\uFF09": "Keywords (top 50)",
7407
+ "\u4F18\u5316\u5EFA\u8BAE": "Recommendations",
7408
+ "\u672C\u671F\u6D88\u8017/\u5C55\u793A/\u70B9\u51FB/\u8F6C\u5316/CTR/CPC/CPA \u6982\u89C8": "Spend, impressions, clicks, conversions, CTR, CPC, and CPA",
7409
+ "\u6309\u65E5\u6D88\u8017 / \u8F6C\u5316 / CPA \u66F2\u7EBF": "Daily spend, conversions, and CPA",
7410
+ "\u5168\u5468\u671F\u7D2F\u8BA1\u6838\u5FC3\u6307\u6807\u4E0E\u641C\u7D22\u66DD\u5149\u4EFD\u989D": "Period totals and search impression share",
7411
+ "\u6309\u6D88\u8017\u6392\u5E8F\uFF0C\u542B\u72B6\u6001\u4E0E\u9884\u7B97/\u51FA\u4EF7\u7C7B\u578B": "Sorted by spend, with status and budget or bid type",
7412
+ "PC / \u79FB\u52A8 / \u5E73\u677F \u6D88\u8017\u4E0E\u6548\u679C\u5360\u6BD4": "Spend and results by computer, mobile, and tablet",
7413
+ "\u6309\u6D88\u8017\u6392\u5E8F\u7684\u56FD\u5BB6/\u5730\u533A\u8868\u73B0": "Countries and regions by spend",
7414
+ "\u8DE8\u7CFB\u5217\u5408\u5E76\u5C55\u793A\uFF0C\u6309\u6D88\u8017\u6392\u5E8F": "Combined across campaigns, sorted by spend",
7415
+ "\u7EFC\u5408\u4EE5\u4E0A\u6570\u636E\u7684\u53EF\u6267\u884C\u6539\u8FDB\u5EFA\u8BAE": "Actions based on the data above",
7416
+ "\u5F53\u524D\u8D26\u6237\u5DF2\u6709\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u7CFB\u5217 / \u5E7F\u544A\u7EC4 / \u8D26\u6237\uFF09\uFF0C\u4E0D\u662F\u672C\u5468\u671F\u6D88\u8017": "Negative keywords already on the account (campaign, ad group, or account), not this period's spend",
7417
+ "AdGroupAudienceData\uFF1A\u610F\u5411/\u5174\u8DA3/\u518D\u8425\u9500/\u5BA2\u6237\u5339\u914D\u7B49\u5217\u8868\u8868\u73B0\uFF0C\u4E0D\u662F\u5E74\u9F84\xD7\u6027\u522B": "Ad group audiences (in-market, affinity, remarketing, customer match), not age or gender",
7418
+ "\u641C\u7D22\u5E7F\u544A\u66DD\u5149\u4EFD\u989D": "Search impression share",
7419
+ "\u5C55\u793A\u6B21\u6570": "Impressions",
7420
+ "\u70B9\u51FB\u6B21\u6570": "Clicks",
7421
+ "\u8F6C\u5316\u6B21\u6570": "Conversions",
7422
+ "\u70B9\u51FB\u7387 CTR": "CTR",
7423
+ "\u5E73\u5747\u70B9\u51FB\u8D39\u7528": "Avg. CPC",
7424
+ "\u6BCF\u6B21\u8F6C\u5316\u8D39\u7528 CPA": "CPA",
7425
+ "\u6BCF\u6B21\u8F6C\u5316\u8D39\u7528": "Cost / conv.",
7426
+ "\u6D3B\u8DC3\u5929\u6570": "Active days",
7427
+ "\u65E5\u5747\u6D88\u8017": "Avg. daily spend",
7428
+ "\u5E7F\u544A\u7CFB\u5217": "Campaign",
7429
+ "\u5E7F\u544A\u7EC4": "Ad group",
7430
+ "\u5173\u952E\u8BCD": "Keyword",
7431
+ "\u5339\u914D\u65B9\u5F0F": "Match type",
7432
+ "\u56FD\u5BB6/\u5730\u533A": "Country / region",
7433
+ "\u641C\u7D22\u8BCD": "Search term",
7434
+ "\u5426\u8BCD": "Negative keywords",
7435
+ "\u5426\u5B9A\u8BCD": "Negative keyword",
7436
+ "\u7EA7\u522B": "Level",
7437
+ "\u53D7\u4F17": "Audience",
7438
+ "\u8BBE\u5907": "Device",
7439
+ "\u72B6\u6001": "Status",
7440
+ "\u7C7B\u578B": "Type",
7441
+ "\u5206\u6790": "Analysis",
7442
+ "\u5EFA\u8BAE": "Recommendation",
7443
+ "\u73AF\u6BD4": "vs prior",
7444
+ "\u8D26\u6237": "Account",
7445
+ "\u8D27\u5E01": "Currency",
7446
+ "\u6D88\u8017": "Spend",
7447
+ "\u5C55\u793A": "Impr.",
7448
+ "\u70B9\u51FB": "Clicks",
7449
+ "\u8F6C\u5316": "Conv.",
7450
+ "\u82B1\u8D39": "Spend",
7451
+ "\u6D88\u8D39": "Spend",
7452
+ "\u9884\u7B97": "Budget",
7453
+ "\u4F59\u989D": "Balance",
7454
+ "\u5B63\u5EA6": "Quarter",
7455
+ "\u6708\u5EA6": "Month",
7456
+ "\u5468\u671F": "Period",
7457
+ "\u672C\u671F": "This period",
7458
+ "\u4E0A\u671F": "Prior period",
7459
+ "\u4E0B\u671F": "Next period",
7460
+ "\u672C\u8282\u6682\u65E0\u6570\u636E": "No data in this section",
7461
+ "\u6682\u65E0\u6570\u636E": "No data",
7462
+ "\u672A\u63D0\u4F9B": "Not provided",
7463
+ "\u672A\u586B\u5199": "Not filled in",
7464
+ "\u672A\u547D\u540D": "Unnamed",
7465
+ "\u672A\u8BBE\u7F6E": "Not set",
7466
+ "\u672A\u542F\u7528": "Off",
7467
+ "\u5DF2\u542F\u7528": "On",
7468
+ "\u5DF2\u6682\u505C": "Paused",
7469
+ "\u5DF2\u79FB\u9664": "Removed",
7470
+ "\u6295\u653E\u4E2D": "Active",
7471
+ "\u672A\u77E5": "Unknown",
7472
+ "\u826F\u597D": "Good",
7473
+ "\u4F18\u79C0": "Excellent",
7474
+ "\u8FBE\u6807": "Met",
7475
+ "\u4E0D\u8FBE\u6807": "Not met",
7476
+ "\u5F85\u4F18\u5316": "Needs work",
7477
+ "\u6570\u636E\u6765\u6E90": "Source",
7478
+ "\u8D26\u6237\u540D\u79F0": "Account name",
7479
+ "\u8D26\u6237\u62A5\u544A": "Account report",
7480
+ "\u8D26\u6237\u603B\u89C8": "Account overview",
7481
+ "\u8D26\u6237\u7ED3\u6784": "Account structure",
7482
+ "\u5E7F\u544A\u7CFB\u5217\u62A5\u544A": "Campaign report",
7483
+ "\u641C\u7D22\u5173\u952E\u5B57\u62A5\u544A": "Keyword report",
7484
+ "\u641C\u7D22\u5B57\u8BCD\u62A5\u544A": "Search terms report",
7485
+ "\u8BBE\u5907\u62A5\u544A": "Device report",
7486
+ "\u5730\u7406\u4F4D\u7F6E\u62A5\u544A": "Geographic report",
7487
+ "\u5E73\u5747\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528": "Avg. CPC",
7488
+ "\u6240\u6709\u8F6C\u5316\u6B21\u6570": "All conversions",
7489
+ "\u8F6C\u5316\u7387": "Conv. rate",
7490
+ "\u5339\u914D\u7C7B\u578B": "Match type",
7491
+ "\u5DF2\u6DFB\u52A0/\u5DF2\u6392\u9664": "Added / excluded",
7492
+ "\u51FA\u4EF7\u8C03\u6574": "Bid adjustment",
7493
+ "\u4E92\u52A8\u6B21\u6570": "Interactions",
7494
+ "\u4E92\u52A8\u7387": "Interaction rate",
7495
+ "\u5730\u7406\u4F4D\u7F6E": "Location",
7496
+ "\u641C\u7D22\u5B57\u8BCD": "Search term",
7497
+ "\u5173\u952E\u5B57": "Keyword",
7498
+ "\u603B\u8BA1": "Total",
7499
+ "\u8986\u76D6\u4EBA\u6570": "Reach",
7500
+ "\u9891\u6B21": "Frequency",
7501
+ "\u5F52\u56E0\u8BBE\u7F6E": "Attribution",
7502
+ "\u6210\u6548\u7C7B\u578B": "Result type",
7503
+ "\u5355\u6B21\u6210\u6548\u8D39\u7528": "Cost per result",
7504
+ "\u5DF2\u82B1\u8D39\u91D1\u989D": "Amount spent",
7505
+ "\u6C47\u603B\u6570\u636E": "Summary",
7506
+ "\u65E5\u8D8B\u52BF": "Daily trend",
7507
+ "\u56FD\u5BB6": "Country",
7508
+ "\u65B9\u6848\u603B\u89C8": "Plan overview",
7509
+ "1-\u5BA2\u6237\u753B\u50CF": "1-Customer profile",
7510
+ "2-\u8D26\u6237\u7ED3\u6784": "2-Account structure",
7511
+ "3-\u8868\u5355\u8BE2\u76D8C1": "3-Lead form C1",
7512
+ "\u5BA2\u6237\u753B\u50CF": "Customer profile",
7513
+ "\u5BA2\u6237\u540D\u79F0": "Customer",
7514
+ "Facebook \u4E3B\u9875": "Facebook page",
7515
+ "\u5B98\u65B9\u7F51\u7AD9": "Website",
7516
+ "\u4E3B\u8425\u4E1A\u52A1": "Business",
7517
+ "\u6838\u5FC3\u4EA7\u54C1": "Products",
7518
+ "\u6838\u5FC3\u5356\u70B9": "Selling points",
7519
+ "\u76EE\u6807\u5E02\u573A": "Target market",
7520
+ "\u8425\u9500\u76EE\u6807": "Goal",
7521
+ "\u65E5\u9884\u7B97": "Daily budget",
7522
+ "\u5E7F\u544A\u7ED3\u6784": "Ad structure",
7523
+ "\u6295\u653E\u7B56\u7565": "Strategy",
7524
+ "\u5E7F\u544A\u8D26\u6237\u7ED3\u6784": "Ad account structure",
7525
+ "\u5F85 pages \u540E\u8865": "Fill in after pages load",
7526
+ "\u8868\u5355\u8BE2\u76D8\uFF08Lead Form\uFF09": "Lead form",
7527
+ "\u8868\u5355\u8BE2\u76D8": "Lead form",
7528
+ "\u8BCA\u65AD\u65F6\u95F4": "Diagnosed at",
7529
+ "\u7F51\u7AD9\u5185\u5BB9\u53CA\u7ED3\u6784": "Site content and structure",
7530
+ "\u8425\u9500\u57FA\u7840\u4E0E\u5E7F\u544A\u843D\u5730\u9875\u8BCA\u65AD": "Marketing basics and landing pages",
7531
+ "\u7528\u6237\u4F53\u9A8C\u4E0E\u8F6C\u5316\u5206\u6790\u8BCA\u65AD": "UX and conversion",
7532
+ "\u7F51\u7AD9\u6027\u80FD\u8BCA\u65AD": "Site performance",
7533
+ "\u4F18\u52BF": "Strengths",
7534
+ "\u4E0D\u8DB3": "Gaps",
7535
+ "\u4F18\u5148\u7EA7": "Priority",
7536
+ "\u5173\u952E\u884C\u52A8": "Actions",
7537
+ "\u9884\u671F\u6548\u679C": "Expected outcome",
7538
+ "\u4F18\u5316\u91CD\u70B9": "Focus",
7539
+ "\u67D0\u67D0\u67D0\u6709\u9650\u516C\u53F8": "Sample Company Ltd.",
7540
+ "\u8BE2\u76D8/\u8F6C\u5316": "Leads / conversions",
7541
+ "Google Ads \u8D26\u6237": "Google Ads account",
7542
+ "TikTok \u5E7F\u544A\u4E3B\u8D26\u6237": "TikTok advertiser",
7543
+ "Bing \u5E7F\u544A\u8D26\u6237": "Bing Ads account",
7544
+ "Meta \u5E7F\u544A\u8D26\u6237": "Meta ad account",
7545
+ "Yandex \u5E7F\u544A\u8D26\u6237": "Yandex Ads account",
7546
+ "Facebook \u5E7F\u544A\u8D26\u6237": "Facebook ad account",
7547
+ "\u5206\u6790\u62A5\u544A": " report",
7548
+ "\u6BCF\u65E5\u8D8B\u52BF": "Daily trend",
7549
+ "\u7CFB\u5217\u8868\u73B0": "Campaigns",
7550
+ "\u4E00\u7EA7\u884C\u653F\u533A": "Admin area",
7551
+ "\u4E0B\u94BB": "drill-down",
7552
+ "\u6682\u65E0\u5EFA\u8BAE": "No recommendation",
7553
+ "\u6682\u65E0\u4F18\u5316\u5EFA\u8BAE": "No recommendation",
7554
+ "\u6682\u65E0\u5206\u6790\u7ED3\u8BBA": "No analysis",
7555
+ "\u6682\u65E0\u6838\u5FC3\u6307\u6807\u6570\u636E": "No KPI data",
7556
+ "\u6682\u65E0\u8D26\u6237\u7ED3\u6784\u6570\u636E": "No structure data",
7557
+ "CLI \u7B80\u6613\u8BCA\u65AD\uFF08Lighthouse API \u5931\u8D25\uFF09": "Simplified diagnosis (Lighthouse API failed)",
7558
+ "\u57FA\u7840\u8FBE\u6807\u3002(Basic compliance.)": "Basic compliance.",
7559
+ "\u826F\u597D\u3002(Good.)": "Good.",
7560
+ "\u6240\u6709\u6307\u6807\u5747\u672A\u8FBE\u6807\uFF0C\u9700\u8981\u5168\u9762\u4F18\u5316\u3002(All metrics failed, comprehensive optimization needed.)": "All metrics failed. A full optimization pass is needed.",
7561
+ "\u6570\u636E\u83B7\u53D6\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7AD9\u914D\u7F6E\u3002(Data fetch failed, please check website configuration.)": "Data fetch failed. Check the site configuration.",
7562
+ "\u4E0D\u5EFA\u8BAE\u6295\u653E\u5E7F\u544A\uFF0C\u9700\u8981\u5168\u9762\u6574\u6539": "Not ready for ads. Fix the site first.",
7563
+ "\u5C0F\u5E45\u4F18\u5316\u540E\u53EF\u76F4\u63A5\u6295\u653E\u5E7F\u544A": "Ready for ads after a few fixes",
7564
+ "\u53EF\u76F4\u63A5\u6295\u653E\u5E7F\u544A": "Ready for ads",
7565
+ "\u6025\u9700\u4F18\u5316\uFF01": "Needs urgent work",
7566
+ "\u9700\u8981\u91CD\u70B9\u4F18\u5316": "Needs focused work",
7567
+ "\u7F51\u7AD9\u8868\u73B0\u4F18\u79C0\uFF0C\u8FDC\u9AD8\u4E8E\u884C\u4E1A\u5E73\u5747\u503C\uFF0C\u53EF\u653E\u5FC3\u5F00\u542F\u5E7F\u544A\u6295\u653E": "The site is well above the industry average and is ready for ads",
7568
+ "\u7F51\u7AD9\u52A0\u8F7D\u901F\u5EA6\u5FEB\uFF0C\u9875\u9762\u54CD\u5E94\u53CA\u65F6\uFF0C\u8FBE\u5230\u884C\u4E1A\u9886\u5148\u6C34\u5E73": "The site loads quickly and is ahead of the industry",
7569
+ "\u7F51\u7AD9\u52A0\u8F7D\u901F\u5EA6\u8F83\u6162\uFF0C\u5EFA\u8BAE\u8FDB\u884C\u5168\u9762\u4F18\u5316\uFF0C\u63D0\u5347\u9875\u9762\u6027\u80FD": "The site is slow. Improve performance before scaling ads",
7570
+ "\u62A5\u544A\u6D4F\u89C8\u903B\u8F91": "How to read this report",
7571
+ "\u6295\u653E\u6548\u7387\u603B\u89C8": "Efficiency overview",
7572
+ "\u9884\u7B97\u82B1\u5728\u54EA\uFF08\u5E7F\u544A\u7C7B\u578B\uFF09": "Where the budget went (ad types)",
7573
+ "\u8BE2\u76D8\u4ECE\u54EA\u6765\uFF08\u533A\u57DF\u7ED3\u6784\uFF09": "Where leads came from (regions)",
7574
+ "\u56FD\u5BB6\u660E\u7EC6\uFF1A\u6D88\u8D39 \xB7 \u8BE2\u76D8 \xB7 \u6210\u672C": "Country detail: spend \xB7 leads \xB7 cost",
7575
+ "\u533A\u57DF\u4E0E\u56FD\u5BB6\uFF1A\u52A0 / \u5B88 / \u780D": "Regions and countries: add / hold / cut",
7576
+ "\u641C\u7D22\u4F18\u5316\u6253\u6CD5\uFF08\u57FA\u672C\u76D8\uFF09": "Search optimization (core)",
7577
+ "\u975E\u641C\u7D22\uFF1A\u6309\u7C7B\u578B\u6267\u884C\u6E05\u5355": "Non-search: action list by type",
7578
+ "\u5468\u8282\u594F\u4E0E\u6267\u884C\u6E05\u5355": "Weekly cadence and action list",
7579
+ "\u6708\u5EA6\u76EE\u6807\u9636\u68AF\uFF08\u5EFA\u8BAE\u6863\u82B1\u8D39\uFF0C\u4E0D\u662F\u5DF2\u6279\u9884\u7B97\uFF09": "Monthly target ladder (suggested spend, not approved budget)",
7580
+ "\u6838\u5FC3 7 \u7EF4\uFF08\u5BF9\u6807\u6295\u653E\u62A5\u8868\uFF09": "7 core metrics (vs the ads report)",
7581
+ "\u6708\u5EA6\u5168\u7EF4\u5EA6\u660E\u7EC6": "Monthly full-dimension detail",
7582
+ "\u5E7F\u544A\u63A2\u7D22\uFF08\u975E\u641C\u7D22\uFF09": "Ad exploration (non-search)",
7583
+ "\u5185\u5BB9\u6C47\u603B\uFF1A\u4EAE\u70B9\u4E0E\u6CE8\u610F": "Highlights and watch-outs",
7584
+ "\u8D26\u6237\u4FA7\u89C2\u5BDF": "Account observations",
7585
+ "\u6570\u636E\u89C4\u5212": "Planning",
7586
+ "\u6570\u636E\u8868\u73B0": "Results",
7587
+ "\u89C4\u5212\u6838\u5FC3": "Plan focus",
7588
+ "\u9884\u7B97\u600E\u4E48\u5206": "How to split the budget",
7589
+ "\u8BE2\u76D8\u6570\u91CF + \u8D28\u91CF": "Lead volume and quality",
7590
+ "\u54C1\u724C\u58F0\u91CF": "Brand reach",
7591
+ "\u914D\u6BD4\u7EAA\u5F8B": "Mix rules",
7592
+ "\u843D\u5730\u9875\u4E0E\u7EBF\u7D22\u8D28\u91CF": "Landing pages and lead quality",
7593
+ "\u53D7\u4F17\u4E0E\u8FD0\u8425\u52A8\u4F5C": "Audiences and ops",
7594
+ "\u89C6\u9891\u53E3\u5F84\uFF08\u4E0D\u662F\u672C\u6237\u89C4\u5212\uFF09": "Video notes (not this account's plan)",
7595
+ "\u4E09\u6708\u8DEF\u7EBF\u56FE": "Three-month roadmap",
7596
+ "\u5EFA\u8BAE\u6863\u6708\u5EA6\u62C6\u5206": "Suggested monthly split",
7597
+ "\u76F8\u5173\u641C\u7D22\u8BCD": "Related search terms",
7598
+ "\u6838\u5FC3\u7ED3\u8BBA": "Key takeaways",
7599
+ "\u9605\u8BFB\u8DEF\u5F84": "Reading path",
7600
+ "\u9884\u7B97\u7ED3\u6784": "Budget mix",
7601
+ "\u533A\u57DF\u7ED3\u6784": "Region mix",
7602
+ "\u56FD\u5BB6\u660E\u7EC6": "Country detail",
7603
+ "\u8D26\u6237\u89C2\u5BDF": "Account notes",
7604
+ "\u7ED3\u6784\u4F18\u5316": "Structure",
7605
+ "\u63A2\u7D22\u843D\u5730": "Exploration",
7606
+ "\u901A\u7528\u5DE1\u68C0": "Routine checks",
7607
+ "\u9884\u7B97\u914D\u6BD4": "Budget mix",
7608
+ "\u5E7F\u544A\u63A2\u7D22": "Ad exploration",
7609
+ "\u8BE2\u76D8\u4FA7\u8865\u5145": "Lead-side extras",
7610
+ "\u76EE\u6807\u5206\u5C42": "Goal tiers",
7611
+ "\u8C22\u8C22": "Thank you"
7612
+ };
7613
+ }
7614
+ });
7615
+
7616
+ // src/report-i18n/html.ts
7617
+ function localizeReportHtml(html, lang) {
7618
+ const withLang = setHtmlLang(html, lang);
7619
+ const phrased = lang === "en" ? applyEnglishPhrases(withLang) : withLang;
7620
+ return injectReportLabels(phrased, lang);
7621
+ }
7622
+ function setHtmlLang(html, lang) {
7623
+ const attr = htmlLangAttr(lang);
7624
+ if (/<html\b[^>]*\blang\s*=/i.test(html)) {
7625
+ return html.replace(/(<html\b[^>]*\blang\s*=\s*)(["'])[^"']*\2/i, `$1"${attr}"`);
7626
+ }
7627
+ return html.replace(/<html\b/i, `<html lang="${attr}"`);
7628
+ }
7629
+ function applyEnglishPhrases(html) {
7630
+ const keys = Object.keys(ENGLISH_PHRASES).filter((key) => key.length >= 2).sort((a, b) => b.length - a.length);
7631
+ let out = html;
7632
+ for (const zh of keys) {
7633
+ const en = ENGLISH_PHRASES[zh];
7634
+ if (!en || en === zh || !out.includes(zh)) continue;
7635
+ out = out.split(zh).join(en);
7636
+ }
7637
+ return out;
7638
+ }
7639
+ function injectReportLabels(html, lang) {
7640
+ const json = JSON.stringify(kpiLabels(lang)).replace(/</g, "\\u003c");
7641
+ const assignment = `window.__REPORT_LABELS__ = ${json};`;
7642
+ if (html.includes(LABELS_MARKER)) return html.replace(LABELS_MARKER, assignment);
7643
+ const tag = `<script>${assignment}</script>`;
7644
+ if (html.includes("</head>")) return html.replace("</head>", `${tag}
7645
+ </head>`);
7646
+ return `${tag}
7647
+ ${html}`;
7648
+ }
7649
+ var LABELS_MARKER;
7650
+ var init_html = __esm({
7651
+ "src/report-i18n/html.ts"() {
7652
+ "use strict";
7653
+ init_lang();
7654
+ init_labels();
7655
+ init_phrases_en();
7656
+ LABELS_MARKER = "window.__REPORT_LABELS__ = window.__REPORT_LABELS__ || null;";
7657
+ }
7658
+ });
7659
+
7232
7660
  // src/utils/google-analysis-snapshot.ts
7233
7661
  var init_google_analysis_snapshot = __esm({
7234
7662
  "src/utils/google-analysis-snapshot.ts"() {
@@ -7579,19 +8007,20 @@ var init_geo_en2zh = __esm({
7579
8007
  });
7580
8008
 
7581
8009
  // src/commands/report-period.ts
7582
- function classifyReportPeriod(current, previous) {
8010
+ function classifyReportPeriod(current, previous, lang = "zh") {
7583
8011
  const start = tryParse(current?.start);
7584
8012
  const end = tryParse(current?.end);
7585
- if (!start || !end) return customPeriod();
8013
+ if (!start || !end) return customPeriod(lang);
7586
8014
  if (isFullCalendarQuarter(start, end)) {
7587
8015
  const tag = quarterTagFromMonth(start.m);
7588
8016
  const prev = tryParse(previous?.start);
7589
8017
  const prevEnd = tryParse(previous?.end);
7590
8018
  return {
7591
8019
  current: tag,
7592
- previous: prev && prevEnd && isFullCalendarQuarter(prev, prevEnd) ? quarterTagFromMonth(prev.m) : "\u4E0A\u671F",
8020
+ previous: prev && prevEnd && isFullCalendarQuarter(prev, prevEnd) ? quarterTagFromMonth(prev.m) : priorWord(lang),
7593
8021
  next: tag === "Q4" ? "Q1" : `Q${Number(tag.slice(1)) + 1}`,
7594
- unit: "\u5B63\u5EA6"
8022
+ unit: "\u5B63\u5EA6",
8023
+ unitLabel: lang === "en" ? "Quarterly" : "\u5B63\u5EA6"
7595
8024
  };
7596
8025
  }
7597
8026
  if (start.d === 1 && start.y === end.y && start.m === end.m) {
@@ -7600,13 +8029,14 @@ function classifyReportPeriod(current, previous) {
7600
8029
  const prevSameMonth = prev && prevEnd && prev.y === prevEnd.y && prev.m === prevEnd.m;
7601
8030
  const nextMonth = start.m === 12 ? 1 : start.m + 1;
7602
8031
  return {
7603
- current: `${start.m}\u6708`,
7604
- previous: prevSameMonth ? `${prev.m}\u6708` : "\u4E0A\u671F",
7605
- next: `${nextMonth}\u6708`,
7606
- unit: "\u6708\u5EA6"
8032
+ current: monthWord(start.m, lang),
8033
+ previous: prevSameMonth ? monthWord(prev.m, lang) : priorWord(lang),
8034
+ next: monthWord(nextMonth, lang),
8035
+ unit: "\u6708\u5EA6",
8036
+ unitLabel: lang === "en" ? "Monthly" : "\u6708\u5EA6"
7607
8037
  };
7608
8038
  }
7609
- return customPeriod();
8039
+ return customPeriod(lang);
7610
8040
  }
7611
8041
  function rangeFromMeta(meta) {
7612
8042
  if (!meta || typeof meta !== "object") return null;
@@ -7614,14 +8044,24 @@ function rangeFromMeta(meta) {
7614
8044
  if (typeof record2.startDate !== "string" || typeof record2.endDate !== "string") return null;
7615
8045
  return { start: record2.startDate, end: record2.endDate };
7616
8046
  }
7617
- function attachPeriodChrome(payload) {
7618
- return { ...payload, periodChrome: classifyReportPeriod(rangeFromMeta(payload.meta)) };
8047
+ function attachPeriodChrome(payload, lang = "zh") {
8048
+ return { ...payload, periodChrome: classifyReportPeriod(rangeFromMeta(payload.meta), void 0, lang) };
7619
8049
  }
7620
8050
  function periodWord(unit, neutral) {
7621
8051
  return unit || neutral;
7622
8052
  }
7623
- function customPeriod() {
7624
- return { current: "\u672C\u671F", previous: "\u4E0A\u671F", next: "\u4E0B\u671F", unit: "" };
8053
+ function customPeriod(lang) {
8054
+ if (lang === "en") {
8055
+ return { current: "This period", previous: "Prior period", next: "Next period", unit: "", unitLabel: "" };
8056
+ }
8057
+ return { current: "\u672C\u671F", previous: "\u4E0A\u671F", next: "\u4E0B\u671F", unit: "", unitLabel: "" };
8058
+ }
8059
+ function priorWord(lang) {
8060
+ return lang === "en" ? "Prior period" : "\u4E0A\u671F";
8061
+ }
8062
+ function monthWord(month, lang) {
8063
+ if (lang === "en") return MONTHS_EN[month - 1] ?? String(month);
8064
+ return `${month}\u6708`;
7625
8065
  }
7626
8066
  function tryParse(iso) {
7627
8067
  if (!iso) return null;
@@ -7644,10 +8084,11 @@ function isFullCalendarQuarter(start, end) {
7644
8084
  function quarterTagFromMonth(month) {
7645
8085
  return `Q${Math.ceil(month / 3)}`;
7646
8086
  }
7647
- var DATE_RE2;
8087
+ var MONTHS_EN, DATE_RE2;
7648
8088
  var init_report_period = __esm({
7649
8089
  "src/commands/report-period.ts"() {
7650
8090
  "use strict";
8091
+ MONTHS_EN = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
7651
8092
  DATE_RE2 = /^(\d{4})-(\d{2})-(\d{2})/;
7652
8093
  }
7653
8094
  });
@@ -103235,19 +103676,20 @@ var init_city_to_country = __esm({
103235
103676
  });
103236
103677
 
103237
103678
  // src/commands/google-analysis/format-campaign-status.ts
103238
- function formatGoogleCampaignStatusDisplay(status) {
103679
+ function formatGoogleCampaignStatusDisplay(status, lang = "zh") {
103239
103680
  if (status == null) return "\u2014";
103240
103681
  const raw = String(status).trim();
103241
103682
  if (raw === "") return "\u2014";
103683
+ const en = lang === "en";
103242
103684
  switch (raw.toUpperCase()) {
103243
103685
  case "ENABLED":
103244
- return "\u6295\u653E\u4E2D";
103686
+ return en ? "Active" : "\u6295\u653E\u4E2D";
103245
103687
  case "PAUSED":
103246
- return "\u5DF2\u6682\u505C";
103688
+ return en ? "Paused" : "\u5DF2\u6682\u505C";
103247
103689
  case "REMOVED":
103248
- return "\u5DF2\u79FB\u9664";
103690
+ return en ? "Removed" : "\u5DF2\u79FB\u9664";
103249
103691
  default:
103250
- return "\u672A\u77E5";
103692
+ return en ? "Unknown" : "\u672A\u77E5";
103251
103693
  }
103252
103694
  }
103253
103695
  function readGoogleCampaignStatusRaw(row) {
@@ -104558,17 +105000,31 @@ function formatOkkiZhDateRange(start, end) {
104558
105000
  if (!a || !b) return [start, end].filter(Boolean).join(" - ") || "\u2014";
104559
105001
  return `${a.y}\u5E74${a.m}\u6708${a.d}\u65E5 - ${b.y}\u5E74${b.m}\u6708${b.d}\u65E5`;
104560
105002
  }
104561
- function formatOkkiReviewTitle(endDate) {
105003
+ function formatOkkiReviewTitle(endDate, lang = "zh") {
104562
105004
  const parsed = parseYmd(endDate);
104563
105005
  const month = parsed?.m ?? Number(String(endDate).split("-")[1]);
104564
- if (!Number.isFinite(month) || month < 1) return "\u6570\u636E\u590D\u76D8\uFF1A";
105006
+ if (!Number.isFinite(month) || month < 1) return lang === "en" ? "Performance review:" : "\u6570\u636E\u590D\u76D8\uFF1A";
105007
+ if (lang === "en") return `Performance review for month ${month}:`;
104565
105008
  return `${month}\u6708\u4EFD\u6570\u636E\u590D\u76D8\uFF1A`;
104566
105009
  }
104567
- function deviceLabel(value) {
105010
+ function formatOkkiDateRange(start, end, lang) {
105011
+ if (lang !== "en") return formatOkkiZhDateRange(start, end);
105012
+ const a = parseYmd(start);
105013
+ const b = parseYmd(end);
105014
+ if (!a || !b) return [start, end].filter(Boolean).join(" - ") || "\u2014";
105015
+ const iso = (p) => `${p.y}-${String(p.m).padStart(2, "0")}-${String(p.d).padStart(2, "0")}`;
105016
+ return `${iso(a)} \u2013 ${iso(b)}`;
105017
+ }
105018
+ function deviceLabel(value, lang) {
104568
105019
  const raw = str(value);
105020
+ if (lang === "en") return raw || "\u2014";
104569
105021
  return DEVICE_TYPE_ZH[raw] ?? raw;
104570
105022
  }
104571
- function targetingStatus(row) {
105023
+ function targetingStatus(row, lang = "zh") {
105024
+ if (lang === "en") {
105025
+ const raw2 = row.queryTargetingStatus ?? row.matchType;
105026
+ if (textLen(raw2)) return String(raw2).trim();
105027
+ }
104572
105028
  if (textLen(row.queryTargetingStatusZh)) return String(row.queryTargetingStatusZh).trim();
104573
105029
  const raw = str(row.queryTargetingStatus, "");
104574
105030
  return TARGETING_STATUS_ZH[raw] ?? (raw || "\u2014");
@@ -104657,10 +105113,11 @@ function campaignRows(data) {
104657
105113
  ]
104658
105114
  };
104659
105115
  }
104660
- function keywordRows(data) {
105116
+ function keywordRows(data, lang = "zh") {
105117
+ const matchOf = (row) => lang === "en" ? row.keywordMatchType || row.keywordMatchTypeZh : row.keywordMatchTypeZh || row.keywordMatchType;
104661
105118
  return sortBySpendDesc(data.tables?.keywords ?? []).map((row) => [
104662
- formatOkkiKeywordDisplay(row.keyword, row.keywordMatchTypeZh || row.keywordMatchType),
104663
- str(row.keywordMatchTypeZh || row.keywordMatchType),
105119
+ formatOkkiKeywordDisplay(row.keyword, matchOf(row)),
105120
+ str(matchOf(row)),
104664
105121
  str(row.campaignName),
104665
105122
  str(row.adGroupName),
104666
105123
  money(row.spend),
@@ -104673,16 +105130,16 @@ function keywordRows(data) {
104673
105130
  rate(row.conversionRate)
104674
105131
  ]);
104675
105132
  }
104676
- function searchTermRows(data) {
105133
+ function searchTermRows(data, lang = "zh") {
104677
105134
  return sortBySpendDesc(data.tables?.searchTerms ?? []).map((row) => [
104678
105135
  str(row.searchTermText),
104679
- str(row.matchTypeZh || row.matchType),
104680
- targetingStatus(row),
105136
+ str(lang === "en" ? row.matchType || row.matchTypeZh : row.matchTypeZh || row.matchType),
105137
+ targetingStatus(row, lang),
104681
105138
  str(row.campaignName),
104682
105139
  str(row.adGroupName),
104683
105140
  formatOkkiKeywordDisplay(
104684
105141
  row.keywordText || row.keyword,
104685
- row.keywordMatchTypeZh || row.keywordMatchType || row.matchTypeZh || row.matchType
105142
+ lang === "en" ? row.keywordMatchType || row.keywordMatchTypeZh || row.matchType || row.matchTypeZh : row.keywordMatchTypeZh || row.keywordMatchType || row.matchTypeZh || row.matchType
104686
105143
  ),
104687
105144
  count(row.impressions),
104688
105145
  count(row.clicks),
@@ -104694,11 +105151,11 @@ function searchTermRows(data) {
104694
105151
  rate(row.conversionRate)
104695
105152
  ]);
104696
105153
  }
104697
- function deviceRows(data) {
105154
+ function deviceRows(data, lang = "zh") {
104698
105155
  return sortBySpendDesc(data.tables?.devices ?? []).map((row) => {
104699
105156
  const pair = conversionsPair(row);
104700
105157
  return [
104701
- deviceLabel(row.deviceType),
105158
+ deviceLabel(row.deviceType, lang),
104702
105159
  "\u5E7F\u544A\u7CFB\u5217",
104703
105160
  str(row.campaignName),
104704
105161
  str(row.bidModifierDisplay, "\u2014"),
@@ -104714,13 +105171,15 @@ function deviceRows(data) {
104714
105171
  ];
104715
105172
  });
104716
105173
  }
104717
- function geoRows(data) {
105174
+ function geoRows(data, lang = "zh") {
104718
105175
  return sortBySpendDesc(data.tables?.geo ?? []).map((row) => {
104719
105176
  const pair = conversionsPair(row);
104720
105177
  const impressions = num6(row.impressions) ?? 0;
104721
105178
  const interactions = okkiGeoInteractionCount(row);
104722
105179
  return [
104723
- str(row.countryOrRegionZh || row.countryNameZh || row.countryOrRegion),
105180
+ str(
105181
+ lang === "en" ? row.countryOrRegion || row.countryNameZh : row.countryOrRegionZh || row.countryNameZh || row.countryOrRegion
105182
+ ),
104724
105183
  str(row.campaignName),
104725
105184
  count(row.impressions),
104726
105185
  interactions > 0 ? count(interactions) : 0,
@@ -104736,9 +105195,9 @@ function geoRows(data) {
104736
105195
  ];
104737
105196
  });
104738
105197
  }
104739
- function buildOkkiWeeklySheetDrafts(data) {
105198
+ function buildOkkiWeeklySheetDrafts(data, lang = "zh") {
104740
105199
  const meta = asRecord21(data.meta) ?? {};
104741
- const period = formatOkkiZhDateRange(str(meta.startDate, ""), str(meta.endDate, ""));
105200
+ const period = formatOkkiDateRange(str(meta.startDate, ""), str(meta.endDate, ""), lang);
104742
105201
  const campaigns = campaignRows(data);
104743
105202
  const drafts = {
104744
105203
  campaigns: {
@@ -104762,7 +105221,7 @@ function buildOkkiWeeklySheetDrafts(data) {
104762
105221
  dataRows: campaigns.body,
104763
105222
  totalRow: campaigns.total,
104764
105223
  analysisLines: analysisLines(data, "campaigns"),
104765
- reviewTitle: formatOkkiReviewTitle(str(meta.endDate, "")),
105224
+ reviewTitle: formatOkkiReviewTitle(str(meta.endDate, ""), lang),
104766
105225
  reviewLines: reviewLines(data)
104767
105226
  },
104768
105227
  keywords: {
@@ -104784,7 +105243,7 @@ function buildOkkiWeeklySheetDrafts(data) {
104784
105243
  "\u8F6C\u5316\u7387"
104785
105244
  ],
104786
105245
  colWidths: [28, 12, 24, 20, 11, 11, 10, 9, 16, 10, 14, 9],
104787
- dataRows: keywordRows(data),
105246
+ dataRows: keywordRows(data, lang),
104788
105247
  analysisLines: analysisLines(data, "keywords")
104789
105248
  },
104790
105249
  searchTerms: {
@@ -104808,7 +105267,7 @@ function buildOkkiWeeklySheetDrafts(data) {
104808
105267
  "\u8F6C\u5316\u7387"
104809
105268
  ],
104810
105269
  colWidths: [26, 12, 14, 22, 18, 22, 11, 10, 9, 16, 11, 10, 14, 9],
104811
- dataRows: searchTermRows(data),
105270
+ dataRows: searchTermRows(data, lang),
104812
105271
  analysisLines: analysisLines(data, "searchTerms")
104813
105272
  },
104814
105273
  devices: {
@@ -104831,7 +105290,7 @@ function buildOkkiWeeklySheetDrafts(data) {
104831
105290
  "\u8F6C\u5316\u7387"
104832
105291
  ],
104833
105292
  colWidths: [12, 10, 26, 12, 11, 10, 9, 16, 11, 14, 10, 14, 9],
104834
- dataRows: deviceRows(data),
105293
+ dataRows: deviceRows(data, lang),
104835
105294
  analysisLines: analysisLines(data, "devices")
104836
105295
  },
104837
105296
  geo: {
@@ -104854,15 +105313,26 @@ function buildOkkiWeeklySheetDrafts(data) {
104854
105313
  "\u8F6C\u5316\u7387"
104855
105314
  ],
104856
105315
  colWidths: [16, 26, 11, 11, 9, 11, 11, 10, 9, 14, 10, 14, 9],
104857
- dataRows: geoRows(data),
105316
+ dataRows: geoRows(data, lang),
104858
105317
  analysisLines: analysisLines(data, "geo")
104859
105318
  }
104860
105319
  };
104861
- return OKKI_SHEET_KEYS.map((key) => drafts[key]);
105320
+ return OKKI_SHEET_KEYS.map((key) => localizeOkkiDraft(drafts[key], lang));
105321
+ }
105322
+ function localizeOkkiDraft(draft, lang) {
105323
+ if (lang !== "en") return draft;
105324
+ const text3 = (value) => applyEnglishPhrases(value);
105325
+ return {
105326
+ ...draft,
105327
+ name: text3(draft.name),
105328
+ title: text3(draft.title),
105329
+ headers: draft.headers.map(text3),
105330
+ reviewTitle: draft.reviewTitle ? text3(draft.reviewTitle) : draft.reviewTitle
105331
+ };
104862
105332
  }
104863
- function writeOkkiWeeklyExcel(outPath, data) {
105333
+ function writeOkkiWeeklyExcel(outPath, data, lang = "zh") {
104864
105334
  const wb = createExcelWorkbook({ accent: "google" });
104865
- for (const draft of buildOkkiWeeklySheetDrafts(data)) {
105335
+ for (const draft of buildOkkiWeeklySheetDrafts(data, lang)) {
104866
105336
  const sheet = wb.addSheet(draft.name);
104867
105337
  const span = draft.headers.length;
104868
105338
  sheet.setColWidths(draft.colWidths);
@@ -104876,7 +105346,7 @@ function writeOkkiWeeklyExcel(outPath, data) {
104876
105346
  sheet.dataRow(draft.totalRow, { labelColumns: [0] });
104877
105347
  }
104878
105348
  sheet.blankRow();
104879
- sheet.sectionBar("\u5206\u6790", { span });
105349
+ sheet.sectionBar(lang === "en" ? "Analysis" : "\u5206\u6790", { span });
104880
105350
  for (const line of draft.analysisLines) {
104881
105351
  sheet.noteRow(line, span);
104882
105352
  }
@@ -104894,6 +105364,7 @@ var REVIEW_LABELS2, DEVICE_TYPE_ZH, TARGETING_STATUS_ZH;
104894
105364
  var init_okki_render_excel = __esm({
104895
105365
  "src/commands/google-analysis/okki-render-excel.ts"() {
104896
105366
  "use strict";
105367
+ init_html();
104897
105368
  init_xlsx_style_engine();
104898
105369
  init_okki_display();
104899
105370
  init_okki_report_content();
@@ -104960,6 +105431,7 @@ async function runGoogleAnalysisOkkiRender(opts) {
104960
105431
  `);
104961
105432
  process.exit(1);
104962
105433
  }
105434
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
104963
105435
  const contentCheck = validateOkkiWeeklyReportContent(data);
104964
105436
  if (!contentCheck.ok) {
104965
105437
  console.error(`
@@ -104976,7 +105448,7 @@ async function runGoogleAnalysisOkkiRender(opts) {
104976
105448
  opts.out ?? path35.join(path35.dirname(dataPath), "okki-weekly-report.xlsx")
104977
105449
  );
104978
105450
  await fs30.mkdir(path35.dirname(outPath), { recursive: true });
104979
- writeOkkiWeeklyExcel(outPath, data);
105451
+ writeOkkiWeeklyExcel(outPath, data, lang);
104980
105452
  console.log(`
104981
105453
  \u2705 OKKI \u5468\u62A5\u5DF2\u751F\u6210\uFF1A${outPath}
104982
105454
  `);
@@ -104988,6 +105460,7 @@ var init_okki_render_report = __esm({
104988
105460
  init_okki_merge_snapshot();
104989
105461
  init_okki_report_content();
104990
105462
  init_okki_render_excel();
105463
+ init_lang();
104991
105464
  }
104992
105465
  });
104993
105466
 
@@ -105180,12 +105653,13 @@ function asRecord23(value) {
105180
105653
  }
105181
105654
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
105182
105655
  }
105183
- function injectReportData2(html, payload) {
105184
- const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
105185
- if (!html.includes(INJECT_MARKER)) {
105656
+ function injectReportData2(html, payload, lang) {
105657
+ const localized = localizeReportHtml(html, lang);
105658
+ const json = JSON.stringify(attachPeriodChrome(stampMetaLang(payload, lang), lang)).replace(/</g, "\\u003c");
105659
+ if (!localized.includes(INJECT_MARKER)) {
105186
105660
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 google-period-report.html");
105187
105661
  }
105188
- return html.replace(INJECT_MARKER, `window.__GOOGLE_PERIOD_REPORT__ = ${json};`);
105662
+ return localized.replace(INJECT_MARKER, `window.__GOOGLE_PERIOD_REPORT__ = ${json};`);
105189
105663
  }
105190
105664
  async function runGoogleAnalysisRender(opts) {
105191
105665
  const dataPath = path36.resolve(opts.dataFile);
@@ -105193,6 +105667,7 @@ async function runGoogleAnalysisRender(opts) {
105193
105667
  if (opts.snapshotDir?.trim()) {
105194
105668
  data = await mergeGoogleAnalysisSnapshotIntoReport(data, path36.resolve(opts.snapshotDir));
105195
105669
  }
105670
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
105196
105671
  const contentCheck = validateGooglePeriodReportContent(data);
105197
105672
  if (!contentCheck.ok) {
105198
105673
  console.error(`
@@ -105222,7 +105697,7 @@ async function runGoogleAnalysisRender(opts) {
105222
105697
  );
105223
105698
  const outDir = path36.dirname(outPath);
105224
105699
  await fsPromises2.mkdir(outDir, { recursive: true });
105225
- await fsPromises2.writeFile(outPath, injectReportData2(html, data), "utf8");
105700
+ await fsPromises2.writeFile(outPath, injectReportData2(html, data, lang), "utf8");
105226
105701
  const { stats } = contentCheck;
105227
105702
  const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u5468\u671F");
105228
105703
  console.log(`
@@ -105243,6 +105718,8 @@ var init_render_report = __esm({
105243
105718
  init_merge_snapshot();
105244
105719
  init_report_content();
105245
105720
  init_report_period();
105721
+ init_lang();
105722
+ init_html();
105246
105723
  TEMPLATE_BASENAME = "google-period-report.html";
105247
105724
  INJECT_MARKER = "window.__GOOGLE_PERIOD_REPORT__ = window.__GOOGLE_PERIOD_REPORT__ || null;";
105248
105725
  }
@@ -106829,11 +107306,12 @@ function registerGoogleAnalysisCommands(program2) {
106829
107306
  ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 google-period-report.json").option(
106830
107307
  "--snapshot-dir <dir>",
106831
107308
  "\u53EF\u9009\uFF1Agoogle-analysis --json-out \u76EE\u5F55\uFF0C\u81EA\u52A8\u5408\u5E76 KPI / \u56FE\u8868 / \u8868\u683C"
106832
- ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
107309
+ ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(async (opts) => {
106833
107310
  await runGoogleAnalysisRender({
106834
107311
  dataFile: opts.data,
106835
107312
  snapshotDir: opts.snapshotDir,
106836
- out: opts.out
107313
+ out: opts.out,
107314
+ lang: opts.lang
106837
107315
  });
106838
107316
  });
106839
107317
  root.command("okki-render").description(
@@ -106841,11 +107319,12 @@ function registerGoogleAnalysisCommands(program2) {
106841
107319
  ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 okki-weekly-report.json").requiredOption(
106842
107320
  "--snapshot-dir <dir>",
106843
107321
  "google-analysis --json-out \u76EE\u5F55\uFF08\u987B\u542B campaigns / keywords / search-terms / campaign-device / geo-targeted\uFF09"
106844
- ).option("--out <file>", "\u8F93\u51FA xlsx \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\u7684 okki-weekly-report.xlsx\uFF09").action(async (opts) => {
107322
+ ).option("--out <file>", "\u8F93\u51FA xlsx \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\u7684 okki-weekly-report.xlsx\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(async (opts) => {
106845
107323
  await runGoogleAnalysisOkkiRender({
106846
107324
  dataFile: opts.data,
106847
107325
  snapshotDir: opts.snapshotDir,
106848
- out: opts.out
107326
+ out: opts.out,
107327
+ lang: opts.lang
106849
107328
  });
106850
107329
  });
106851
107330
  }
@@ -109378,15 +109857,20 @@ function cell(value) {
109378
109857
  function label(hit) {
109379
109858
  return hit.advertiserName || hit.name || "-";
109380
109859
  }
109381
- function buildBalanceScanDelivery(items3, dataIssueMessage) {
109860
+ function buildBalanceScanDelivery(items3, dataIssueMessage, lang = "zh") {
109861
+ const en = lang === "en";
109382
109862
  if (dataIssueMessage) {
109383
109863
  return { allClear: false, userDelivery: dataIssueMessage, deliveryRule: DELIVERY_RULE };
109384
109864
  }
109385
109865
  const hits = items3.filter((row) => row.hitReason !== "none");
109386
109866
  if (hits.length === 0) {
109387
- return { allClear: true, userDelivery: PATROL_ALL_CLEAR, deliveryRule: DELIVERY_RULE };
109867
+ return {
109868
+ allClear: true,
109869
+ userDelivery: en ? "No hits today" : PATROL_ALL_CLEAR,
109870
+ deliveryRule: DELIVERY_RULE
109871
+ };
109388
109872
  }
109389
- const header = "| \u8D26\u6237\u540D\u79F0 | \u8D26\u6237ID | \u4F59\u989D | \u5E01\u79CD |\n| --- | --- | ---: | --- |";
109873
+ const header = en ? "| Account | Account ID | Balance | Currency |\n| --- | --- | ---: | --- |" : "| \u8D26\u6237\u540D\u79F0 | \u8D26\u6237ID | \u4F59\u989D | \u5E01\u79CD |\n| --- | --- | ---: | --- |";
109390
109874
  const lines = hits.map((h) => {
109391
109875
  const balance = h.balance == null ? "\u2014" : String(h.balance);
109392
109876
  return `| ${cell(label(h))} | ${h.mediaCustomerId} | ${balance} | ${h.currencyCode ?? "\u2014"} |`;
@@ -109636,6 +110120,7 @@ function evaluateBalanceScanAccounts(params) {
109636
110120
  }
109637
110121
 
109638
110122
  // src/commands/balance-scan/run.ts
110123
+ init_lang();
109639
110124
  async function runBalanceScan(opts) {
109640
110125
  let config = loadConfig(opts.token);
109641
110126
  if (opts.refreshDp) {
@@ -109800,7 +110285,8 @@ async function runBalanceScan(opts) {
109800
110285
  totalReported: total ?? null,
109801
110286
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
109802
110287
  };
109803
- const agentBrief = buildBalanceScanDelivery(evaluated, dataIssue?.message ?? null);
110288
+ const lang = readReportLangOrExit({ flag: opts.lang });
110289
+ const agentBrief = buildBalanceScanDelivery(evaluated, dataIssue?.message ?? null, lang);
109804
110290
  const scanPayload = { ok: !dataIssue, data: { items: evaluated }, meta, agentBrief };
109805
110291
  const wroteJson = await emitCliJsonOrSnapshot(opts, {
109806
110292
  section: `balance-scan-${media}`,
@@ -109912,7 +110398,7 @@ function register9(program2) {
109912
110398
  "--json-out <path>",
109913
110399
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
109914
110400
  void 0
109915
- ).option("--refresh-dp", "\u5F3A\u5236\u91CD\u62C9 Datapermission \u540E\u518D\u626B\u63CF\uFF08\u6392\u67E5\u4F1A\u8BDD\u5F02\u5E38\uFF09", false).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
110401
+ ).option("--refresh-dp", "\u5F3A\u5236\u91CD\u62C9 Datapermission \u540E\u518D\u626B\u63CF\uFF08\u6392\u67E5\u4F1A\u8BDD\u5F02\u5E38\uFF09", false).option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).option("--lang <lang>", "\u4EA4\u7ED9\u7528\u6237\u7684\u53E5\u5B50\u8BED\u8A00\uFF1Azh \u6216 en\uFF0C\u9ED8\u8BA4 zh").action(
109916
110402
  async (opts) => {
109917
110403
  await runBalanceScan({
109918
110404
  token: opts.token,
@@ -109928,7 +110414,8 @@ function register9(program2) {
109928
110414
  jsonOut: opts.jsonOut,
109929
110415
  refreshDp: opts.refreshDp,
109930
110416
  verbose: opts.verbose,
109931
- loginCustomerId: opts.loginCustomerId
110417
+ loginCustomerId: opts.loginCustomerId,
110418
+ lang: opts.lang
109932
110419
  });
109933
110420
  }
109934
110421
  );
@@ -110084,11 +110571,15 @@ var PATROL_ALL_CLEAR2 = "\u4ECA\u65E5\u65E0\u547D\u4E2D";
110084
110571
  function cell2(value) {
110085
110572
  return value.replace(/\|/g, "\uFF5C").replace(/\r?\n/g, " ");
110086
110573
  }
110087
- function buildBudgetCircuitDelivery(hits) {
110574
+ function buildBudgetCircuitDelivery(hits, lang = "zh") {
110088
110575
  if (hits.length === 0) {
110089
- return { allClear: true, userDelivery: PATROL_ALL_CLEAR2, deliveryRule: DELIVERY_RULE2 };
110576
+ return {
110577
+ allClear: true,
110578
+ userDelivery: lang === "en" ? "No hits today" : PATROL_ALL_CLEAR2,
110579
+ deliveryRule: DELIVERY_RULE2
110580
+ };
110090
110581
  }
110091
- const header = "| \u7CFB\u5217\u540D\u79F0 | \u7CFB\u5217ID | \u8D26\u6237ID | \u5F53\u65E5\u82B1\u8D39 | \u65E5\u9884\u7B97 |\n| --- | --- | --- | ---: | ---: |";
110582
+ const header = lang === "en" ? "| Campaign | Campaign ID | Account ID | Spend today | Daily budget |\n| --- | --- | --- | ---: | ---: |" : "| \u7CFB\u5217\u540D\u79F0 | \u7CFB\u5217ID | \u8D26\u6237ID | \u5F53\u65E5\u82B1\u8D39 | \u65E5\u9884\u7B97 |\n| --- | --- | --- | ---: | ---: |";
110092
110583
  const lines = hits.map(
110093
110584
  (h) => `| ${cell2(h.campaignName)} | ${h.campaignId} | ${h.mediaCustomerId} | ${h.spend} | ${h.budget} |`
110094
110585
  );
@@ -110098,11 +110589,15 @@ function buildBudgetCircuitDelivery(hits) {
110098
110589
  deliveryRule: DELIVERY_RULE2
110099
110590
  };
110100
110591
  }
110101
- function buildZeroConvDelivery(hits) {
110592
+ function buildZeroConvDelivery(hits, lang = "zh") {
110102
110593
  if (hits.length === 0) {
110103
- return { allClear: true, userDelivery: PATROL_ALL_CLEAR2, deliveryRule: DELIVERY_RULE2 };
110594
+ return {
110595
+ allClear: true,
110596
+ userDelivery: lang === "en" ? "No hits today" : PATROL_ALL_CLEAR2,
110597
+ deliveryRule: DELIVERY_RULE2
110598
+ };
110104
110599
  }
110105
- const header = "| \u5BF9\u8C61\u540D\u79F0 | \u5BF9\u8C61ID | \u8D26\u6237ID | \u5F53\u65E5\u82B1\u8D39 |\n| --- | --- | --- | ---: |";
110600
+ const header = lang === "en" ? "| Name | ID | Account ID | Spend today |\n| --- | --- | --- | ---: |" : "| \u5BF9\u8C61\u540D\u79F0 | \u5BF9\u8C61ID | \u8D26\u6237ID | \u5F53\u65E5\u82B1\u8D39 |\n| --- | --- | --- | ---: |";
110106
110601
  const lines = hits.map(
110107
110602
  (h) => `| ${cell2(h.objectName)} | ${h.objectId} | ${h.mediaCustomerId} | ${h.spend} |`
110108
110603
  );
@@ -110113,6 +110608,9 @@ function buildZeroConvDelivery(hits) {
110113
110608
  };
110114
110609
  }
110115
110610
 
110611
+ // src/commands/guard/budget-circuit.ts
110612
+ init_lang();
110613
+
110116
110614
  // src/commands/guard/rules.ts
110117
110615
  function moneyGte(leftYuan, rightYuan) {
110118
110616
  return Math.round(leftYuan * 100) >= Math.round(rightYuan * 100);
@@ -110245,7 +110743,7 @@ async function runBudgetCircuit(opts) {
110245
110743
  },
110246
110744
  hits,
110247
110745
  errors,
110248
- agentBrief: buildBudgetCircuitDelivery(hits)
110746
+ agentBrief: buildBudgetCircuitDelivery(hits, readReportLangOrExit({ flag: opts.lang }))
110249
110747
  };
110250
110748
  const agentBrief = payload.agentBrief;
110251
110749
  if (await emitCliJsonOrSnapshot(opts, {
@@ -110290,6 +110788,7 @@ async function runBudgetCircuit(opts) {
110290
110788
 
110291
110789
  // src/commands/guard/zero-conv.ts
110292
110790
  init_cli_json_snapshot();
110791
+ init_lang();
110293
110792
  function parseLevel(raw) {
110294
110793
  const v = (raw ?? "adgroup").toLowerCase();
110295
110794
  if (v === "adgroup" || v === "ad" || v === "both") return v;
@@ -110443,7 +110942,7 @@ async function runZeroConv(opts) {
110443
110942
  hits,
110444
110943
  skipped,
110445
110944
  errors,
110446
- agentBrief: buildZeroConvDelivery(hits)
110945
+ agentBrief: buildZeroConvDelivery(hits, readReportLangOrExit({ flag: opts.lang }))
110447
110946
  };
110448
110947
  await emitCliJsonOrSnapshot(opts, {
110449
110948
  section: "guard-zero-conv",
@@ -110498,7 +110997,7 @@ function register11(program2) {
110498
110997
  "--json-out <path>",
110499
110998
  "\u843D\u76D8\u76EE\u5F55\u6216 *.json\uFF1Bstdout \u4E00\u884C\u6458\u8981\uFF1B\u4EBA\u7C7B\u53EF\u8BFB\u8868\u59CB\u7EC8\u6253\u5370",
110500
110999
  void 0
110501
- ).option("--refresh-dp", "\u5F3A\u5236\u91CD\u62C9 Datapermission", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF", false).action(
111000
+ ).option("--refresh-dp", "\u5F3A\u5236\u91CD\u62C9 Datapermission", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF", false).option("--lang <lang>", "\u4EA4\u7ED9\u7528\u6237\u7684\u53E5\u5B50\u8BED\u8A00\uFF1Azh \u6216 en\uFF0C\u9ED8\u8BA4 zh").action(
110502
111001
  async (opts) => {
110503
111002
  await runBudgetCircuit({
110504
111003
  media: opts.media,
@@ -110512,7 +111011,8 @@ function register11(program2) {
110512
111011
  jsonOut: opts.jsonOut,
110513
111012
  refreshDp: opts.refreshDp,
110514
111013
  verbose: opts.verbose,
110515
- loginCustomerId: opts.loginCustomerId
111014
+ loginCustomerId: opts.loginCustomerId,
111015
+ lang: opts.lang
110516
111016
  });
110517
111017
  }
110518
111018
  );
@@ -110525,7 +111025,7 @@ function register11(program2) {
110525
111025
  "--fallback-target-cpa <yuan>",
110526
111026
  "\u7EC4\u65E0 targetCpa\uFF08\u6216\u521B\u610F\u7EF4\uFF09\u65F6\u4F7F\u7528\u7684\u76EE\u6807 CPA\uFF08\u5143\uFF09",
110527
111027
  (v) => parseFloat(v)
110528
- ).option("--page-size <n>", "\u8D26\u6237\u6E05\u5355\u5206\u9875\u5927\u5C0F\uFF08\u9ED8\u8BA4 200\uFF09", (v) => parseInt(v, 10)).option("--login-customer-id <id>", LOGIN_CUSTOMER_ID_OPTION_HELP).option("--max-pages <n>", "\u6700\u591A\u626B\u63CF\u9875\u6570\uFF08\u9ED8\u8BA4 20\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF09").option("--json-out <path>", "\u843D\u76D8\u76EE\u5F55\u6216 *.json", void 0).option("--refresh-dp", "\u5F3A\u5236\u91CD\u62C9 Datapermission", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF", false).action(
111028
+ ).option("--page-size <n>", "\u8D26\u6237\u6E05\u5355\u5206\u9875\u5927\u5C0F\uFF08\u9ED8\u8BA4 200\uFF09", (v) => parseInt(v, 10)).option("--login-customer-id <id>", LOGIN_CUSTOMER_ID_OPTION_HELP).option("--max-pages <n>", "\u6700\u591A\u626B\u63CF\u9875\u6570\uFF08\u9ED8\u8BA4 20\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF09").option("--json-out <path>", "\u843D\u76D8\u76EE\u5F55\u6216 *.json", void 0).option("--refresh-dp", "\u5F3A\u5236\u91CD\u62C9 Datapermission", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF", false).option("--lang <lang>", "\u4EA4\u7ED9\u7528\u6237\u7684\u53E5\u5B50\u8BED\u8A00\uFF1Azh \u6216 en\uFF0C\u9ED8\u8BA4 zh").action(
110529
111029
  async (opts) => {
110530
111030
  await runZeroConv({
110531
111031
  media: opts.media,
@@ -110540,7 +111040,8 @@ function register11(program2) {
110540
111040
  jsonOut: opts.jsonOut,
110541
111041
  refreshDp: opts.refreshDp,
110542
111042
  verbose: opts.verbose,
110543
- loginCustomerId: opts.loginCustomerId
111043
+ loginCustomerId: opts.loginCustomerId,
111044
+ lang: opts.lang
110544
111045
  });
110545
111046
  }
110546
111047
  );
@@ -120853,6 +121354,19 @@ var LAYER_TITLE = {
120853
121354
  negative_keyword: "\u5426\u5B9A\u8BCD",
120854
121355
  extension: "\u9644\u52A0\u4FE1\u606F"
120855
121356
  };
121357
+ var LAYER_TITLE_EN = {
121358
+ campaign: "Campaign",
121359
+ location: "Locations",
121360
+ excluded_location: "Excluded locations",
121361
+ adgroup: "Ad group",
121362
+ keyword: "Keywords",
121363
+ ad: "RSA copy",
121364
+ negative_keyword: "Negative keywords",
121365
+ extension: "Extensions"
121366
+ };
121367
+ function layerTitle(layer, lang) {
121368
+ return lang === "en" ? LAYER_TITLE_EN[layer] : LAYER_TITLE[layer];
121369
+ }
120856
121370
  var STATUS_MARK = {
120857
121371
  ok: "\u2705",
120858
121372
  missing: "\u274C",
@@ -120893,53 +121407,59 @@ function printPlanItemsByAdGroup(configFile, layer, layerItems) {
120893
121407
  function mdCell(text3) {
120894
121408
  return text3.replace(/\r?\n/g, " ").replace(/\|/g, "\uFF5C").trim();
120895
121409
  }
120896
- function statusLabel(status) {
121410
+ function statusLabel(status, lang = "zh") {
121411
+ if (lang === "en") {
121412
+ if (status === "ok") return "\u2705 Created";
121413
+ if (status === "missing") return "\u274C Not created";
121414
+ return "\u23ED Not compared";
121415
+ }
120897
121416
  if (status === "ok") return "\u2705 \u5DF2\u521B\u5EFA";
120898
121417
  if (status === "missing") return "\u274C \u672A\u521B\u5EFA";
120899
121418
  return "\u23ED \u672A\u6BD4\u5BF9";
120900
121419
  }
120901
- function buildCampaignCreateStatusMarkdown(result) {
121420
+ function buildCampaignCreateStatusMarkdown(result, lang = "zh") {
121421
+ const en = lang === "en";
120902
121422
  const sc = result.statusCounts;
120903
121423
  const c = result.counts;
120904
- const conclusion = result.ok ? "\u2705 **\u5168\u90E8\u6210\u529F**\uFF1A\u8BA1\u5212\u5185\u5404\u9879\u5747\u5DF2\u5728\u8D26\u6237\u751F\u6548" : `\u274C **\u90E8\u5206\u5931\u8D25**\uFF1A\u4ECD\u6709 **${sc.missing}** \u9879\u672A\u521B\u5EFA\uFF08\u53E6\u6709 ${sc.skipped} \u9879\u56E0\u7236\u7EA7\u7F3A\u5931\u672A\u6BD4\u5BF9\uFF09`;
121424
+ const conclusion = result.ok ? en ? "\u2705 **All created**: every planned item is live on the account" : "\u2705 **\u5168\u90E8\u6210\u529F**\uFF1A\u8BA1\u5212\u5185\u5404\u9879\u5747\u5DF2\u5728\u8D26\u6237\u751F\u6548" : en ? `\u274C **Partly failed**: **${sc.missing}** items were not created (${sc.skipped} skipped because a parent is missing)` : `\u274C **\u90E8\u5206\u5931\u8D25**\uFF1A\u4ECD\u6709 **${sc.missing}** \u9879\u672A\u521B\u5EFA\uFF08\u53E6\u6709 ${sc.skipped} \u9879\u56E0\u7236\u7EA7\u7F3A\u5931\u672A\u6BD4\u5BF9\uFF09`;
120905
121425
  const lines = [
120906
- `# Google \u641C\u7D22\u5E7F\u544A\u521B\u5EFA\u8BE6\u60C5`,
121426
+ en ? `# Google Search campaign creation` : `# Google \u641C\u7D22\u5E7F\u544A\u521B\u5EFA\u8BE6\u60C5`,
120907
121427
  ``,
120908
- `> \u672C\u6587\u6863\u7531 \`ad batch diff\` \u6839\u636E\u5E7F\u544A\u8BA1\u5212 JSON \u4E0E\u8D26\u6237\u5B9E\u51B5\u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u6309\u8868\u683C\u6838\u5BF9\u6BCF\u6761\u6587\u6848/\u5173\u952E\u8BCD\u7684\u521B\u5EFA\u72B6\u6001\u3002`,
121428
+ en ? `> Generated by \`ad batch diff\` from the plan JSON and the live account. Check each keyword and RSA line in the tables.` : `> \u672C\u6587\u6863\u7531 \`ad batch diff\` \u6839\u636E\u5E7F\u544A\u8BA1\u5212 JSON \u4E0E\u8D26\u6237\u5B9E\u51B5\u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u6309\u8868\u683C\u6838\u5BF9\u6BCF\u6761\u6587\u6848/\u5173\u952E\u8BCD\u7684\u521B\u5EFA\u72B6\u6001\u3002`,
120909
121429
  ``,
120910
- `## \u6982\u89C8`,
121430
+ en ? `## Overview` : `## \u6982\u89C8`,
120911
121431
  ``,
120912
- `| \u9879\u76EE | \u5185\u5BB9 |`,
121432
+ en ? `| Item | Value |` : `| \u9879\u76EE | \u5185\u5BB9 |`,
120913
121433
  `| --- | --- |`,
120914
- `| \u8D26\u6237 ID | \`${mdCell(result.accountId)}\` |`,
120915
- `| \u7CFB\u5217\u540D\u79F0 | ${mdCell(result.campaignName || "\u2014")} |`,
120916
- `| \u7CFB\u5217 ID (campaignId) | \`${mdCell(result.campaignId)}\` |`,
120917
- `| \u6279\u91CF\u4EFB\u52A1 | ${mdCell(result.batchId || "\u2014")} / ${mdCell(result.batchStatus || "\u2014")} |`,
120918
- `| \u8BA1\u5212\u914D\u7F6E | \`${mdCell(result.configFile || "\u2014")}\` |`,
120919
- `| \u7ED3\u8BBA | ${conclusion} |`,
121434
+ en ? `| Account ID | \`${mdCell(result.accountId)}\` |` : `| \u8D26\u6237 ID | \`${mdCell(result.accountId)}\` |`,
121435
+ en ? `| Campaign | ${mdCell(result.campaignName || "\u2014")} |` : `| \u7CFB\u5217\u540D\u79F0 | ${mdCell(result.campaignName || "\u2014")} |`,
121436
+ en ? `| Campaign ID | \`${mdCell(result.campaignId)}\` |` : `| \u7CFB\u5217 ID (campaignId) | \`${mdCell(result.campaignId)}\` |`,
121437
+ en ? `| Batch | ${mdCell(result.batchId || "\u2014")} / ${mdCell(result.batchStatus || "\u2014")} |` : `| \u6279\u91CF\u4EFB\u52A1 | ${mdCell(result.batchId || "\u2014")} / ${mdCell(result.batchStatus || "\u2014")} |`,
121438
+ en ? `| Plan file | \`${mdCell(result.configFile || "\u2014")}\` |` : `| \u8BA1\u5212\u914D\u7F6E | \`${mdCell(result.configFile || "\u2014")}\` |`,
121439
+ en ? `| Result | ${conclusion} |` : `| \u7ED3\u8BBA | ${conclusion} |`,
120920
121440
  ``,
120921
- `## \u6570\u91CF\u6C47\u603B`,
121441
+ en ? `## Counts` : `## \u6570\u91CF\u6C47\u603B`,
120922
121442
  ``,
120923
- `| \u5C42\u7EA7 | \u8D26\u6237\u5B9E\u51B5 | \u8BA1\u5212 |`,
121443
+ en ? `| Layer | Live | Planned |` : `| \u5C42\u7EA7 | \u8D26\u6237\u5B9E\u51B5 | \u8BA1\u5212 |`,
120924
121444
  `| --- | ---: | ---: |`,
120925
- `| \u5E7F\u544A\u7EC4 | ${c.liveAdGroups} | ${c.plannedAdGroups} |`,
120926
- `| \u5173\u952E\u8BCD | ${c.liveKeywords} | ${c.plannedKeywords} |`,
120927
- `| \u521B\u610F (RSA) | ${c.liveAds} | ${c.plannedAds} |`,
120928
- `| \u5426\u5B9A\u8BCD | ${c.liveNegativeKeywords} | ${c.plannedNegativeKeywords} |`,
120929
- `| \u9644\u52A0\u4FE1\u606F | ${c.liveExtensions} | ${c.plannedExtensions} |`,
120930
- `| \u6295\u653E\u5730\u57DF | ${c.liveLocations} | ${c.plannedLocations} |`,
120931
- `| \u6392\u9664\u5730\u57DF | ${c.liveExcludedLocations} | ${c.plannedExcludedLocations} |`,
121445
+ `| ${layerTitle("adgroup", lang)} | ${c.liveAdGroups} | ${c.plannedAdGroups} |`,
121446
+ `| ${layerTitle("keyword", lang)} | ${c.liveKeywords} | ${c.plannedKeywords} |`,
121447
+ `| ${layerTitle("ad", lang)} | ${c.liveAds} | ${c.plannedAds} |`,
121448
+ `| ${layerTitle("negative_keyword", lang)} | ${c.liveNegativeKeywords} | ${c.plannedNegativeKeywords} |`,
121449
+ `| ${layerTitle("extension", lang)} | ${c.liveExtensions} | ${c.plannedExtensions} |`,
121450
+ `| ${layerTitle("location", lang)} | ${c.liveLocations} | ${c.plannedLocations} |`,
121451
+ `| ${layerTitle("excluded_location", lang)} | ${c.liveExcludedLocations} | ${c.plannedExcludedLocations} |`,
120932
121452
  ``,
120933
- `**\u9010\u9879\u72B6\u6001\u8BA1\u6570**\uFF1A\u2705 ${sc.ok}\u3000\u274C ${sc.missing}\u3000\u23ED ${sc.skipped}\u3000\u5408\u8BA1 ${result.items.length}`,
121453
+ en ? `**Status counts**: \u2705 ${sc.ok}\u3000\u274C ${sc.missing}\u3000\u23ED ${sc.skipped}\u3000total ${result.items.length}` : `**\u9010\u9879\u72B6\u6001\u8BA1\u6570**\uFF1A\u2705 ${sc.ok}\u3000\u274C ${sc.missing}\u3000\u23ED ${sc.skipped}\u3000\u5408\u8BA1 ${result.items.length}`,
120934
121454
  ``
120935
121455
  ];
120936
121456
  const emptyInPlan = [
120937
- c.plannedExcludedLocations === 0 ? "\u6392\u9664\u5730\u57DF" : "",
120938
- c.plannedNegativeKeywords === 0 ? "\u5426\u5B9A\u8BCD" : ""
121457
+ c.plannedExcludedLocations === 0 ? layerTitle("excluded_location", lang) : "",
121458
+ c.plannedNegativeKeywords === 0 ? layerTitle("negative_keyword", lang) : ""
120939
121459
  ].filter(Boolean);
120940
121460
  if (emptyInPlan.length > 0) {
120941
121461
  lines.push(
120942
- `> \u26A0\uFE0F \u672C\u6B21\u63D0\u4EA4\u7684\u8BA1\u5212 JSON \u4E2D **${emptyInPlan.join("\u3001")}\u4E3A 0 \u6761**\uFF0C\u8D26\u6237\u5185\u4E5F\u5C31\u6CA1\u6709\u8BBE\u7F6E\u3002\u82E5\u6295\u653E\u65B9\u6848\u539F\u672C\u6709\u8FD9\u4E9B\u5185\u5BB9\uFF0C\u8BF4\u660E\u521B\u5EFA\u8FC7\u7A0B\u4E2D\u88AB\u79FB\u9664\uFF0C\u8BF7\u4E0E\u52A9\u624B\u6838\u5BF9\u662F\u5426\u9700\u8981\u8865\u4E0A\u3002`,
121462
+ en ? `> \u26A0\uFE0F The submitted plan JSON has **0 ${emptyInPlan.join(" and ")}**. The account has none either. If the brief included them, they were dropped during creation \u2014 ask the assistant to add them back.` : `> \u26A0\uFE0F \u672C\u6B21\u63D0\u4EA4\u7684\u8BA1\u5212 JSON \u4E2D **${emptyInPlan.join("\u3001")}\u4E3A 0 \u6761**\uFF0C\u8D26\u6237\u5185\u4E5F\u5C31\u6CA1\u6709\u8BBE\u7F6E\u3002\u82E5\u6295\u653E\u65B9\u6848\u539F\u672C\u6709\u8FD9\u4E9B\u5185\u5BB9\uFF0C\u8BF4\u660E\u521B\u5EFA\u8FC7\u7A0B\u4E2D\u88AB\u79FB\u9664\uFF0C\u8BF7\u4E0E\u52A9\u624B\u6838\u5BF9\u662F\u5426\u9700\u8981\u8865\u4E0A\u3002`,
120943
121463
  ``
120944
121464
  );
120945
121465
  }
@@ -120957,15 +121477,18 @@ function buildCampaignCreateStatusMarkdown(result) {
120957
121477
  }
120958
121478
  for (const [groupName, groupItems] of byGroup) {
120959
121479
  if (layer === "keyword") {
120960
- lines.push(`## \u5E7F\u544A\u7EC4\u300C${groupName}\u300D\xB7 \u5173\u952E\u8BCD`, ``);
120961
- lines.push(`| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
121480
+ lines.push(
121481
+ en ? `## Ad group \u201C${groupName}\u201D \xB7 Keywords` : `## \u5E7F\u544A\u7EC4\u300C${groupName}\u300D\xB7 \u5173\u952E\u8BCD`,
121482
+ ``
121483
+ );
121484
+ lines.push(en ? `| Status | Planned |` : `| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
120962
121485
  for (const item of groupItems) {
120963
- lines.push(`| ${statusLabel(item.status)} | ${mdCell(item.plannedContent)} |`);
121486
+ lines.push(`| ${statusLabel(item.status, lang)} | ${mdCell(item.plannedContent)} |`);
120964
121487
  }
120965
121488
  lines.push(``);
120966
121489
  continue;
120967
121490
  }
120968
- lines.push(`## \u5E7F\u544A\u7EC4\u300C${groupName}\u300D\xB7 RSA \u6587\u6848`, ``);
121491
+ lines.push(en ? `## Ad group \u201C${groupName}\u201D \xB7 RSA` : `## \u5E7F\u544A\u7EC4\u300C${groupName}\u300D\xB7 RSA \u6587\u6848`, ``);
120969
121492
  const byAd = /* @__PURE__ */ new Map();
120970
121493
  for (const item of groupItems) {
120971
121494
  const key = item.adLabel ?? "\u2014";
@@ -120974,39 +121497,47 @@ function buildCampaignCreateStatusMarkdown(result) {
120974
121497
  byAd.set(key, list);
120975
121498
  }
120976
121499
  for (const [adLabel, adItems] of byAd) {
120977
- lines.push(`### \u5E7F\u544A\u300C${adLabel}\u300D`, ``);
120978
- lines.push(`| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
121500
+ lines.push(en ? `### Ad \u201C${adLabel}\u201D` : `### \u5E7F\u544A\u300C${adLabel}\u300D`, ``);
121501
+ lines.push(en ? `| Status | Planned |` : `| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
120979
121502
  for (const item of adItems) {
120980
- lines.push(`| ${statusLabel(item.status)} | ${mdCell(item.plannedContent)} |`);
121503
+ lines.push(`| ${statusLabel(item.status, lang)} | ${mdCell(item.plannedContent)} |`);
120981
121504
  }
120982
121505
  lines.push(``);
120983
121506
  }
120984
121507
  }
120985
121508
  continue;
120986
121509
  }
120987
- lines.push(`## ${LAYER_TITLE[layer]}`, ``);
120988
- lines.push(`| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
121510
+ lines.push(`## ${layerTitle(layer, lang)}`, ``);
121511
+ lines.push(en ? `| Status | Planned |` : `| \u72B6\u6001 | \u8BA1\u5212\u5185\u5BB9 |`, `| --- | --- |`);
120989
121512
  for (const item of layerItems) {
120990
- lines.push(`| ${statusLabel(item.status)} | ${mdCell(item.plannedContent)} |`);
121513
+ lines.push(`| ${statusLabel(item.status, lang)} | ${mdCell(item.plannedContent)} |`);
120991
121514
  }
120992
121515
  lines.push(``);
120993
121516
  }
120994
121517
  if (result.missing.length > 0) {
120995
- lines.push(`## \u4ECD\u672A\u521B\u5EFA\u9879\uFF08\u987B\u5173\u6CE8\uFF09`, ``);
120996
- lines.push(`| \u5C42\u7EA7 | \u8BA1\u5212\u5185\u5BB9 | \u8BF4\u660E |`, `| --- | --- | --- |`);
121518
+ lines.push(en ? `## Still missing (needs attention)` : `## \u4ECD\u672A\u521B\u5EFA\u9879\uFF08\u987B\u5173\u6CE8\uFF09`, ``);
121519
+ lines.push(
121520
+ en ? `| Layer | Planned | Note |` : `| \u5C42\u7EA7 | \u8BA1\u5212\u5185\u5BB9 | \u8BF4\u660E |`,
121521
+ `| --- | --- | --- |`
121522
+ );
120997
121523
  for (const m of result.missing) {
120998
121524
  lines.push(
120999
- `| ${LAYER_TITLE[m.layer]} | ${mdCell(m.plannedContent)} | ${mdCell(m.summary)} |`
121525
+ `| ${layerTitle(m.layer, lang)} | ${mdCell(m.plannedContent)} | ${mdCell(m.summary)} |`
121000
121526
  );
121001
121527
  }
121002
121528
  lines.push(``);
121003
121529
  } else {
121004
- lines.push(`## \u4ECD\u672A\u521B\u5EFA\u9879`, ``, `\u65E0\u3002\u8BA1\u5212\u4E0E\u8D26\u6237\u5B9E\u51B5\u4E00\u81F4\u3002`, ``);
121530
+ lines.push(
121531
+ en ? `## Still missing` : `## \u4ECD\u672A\u521B\u5EFA\u9879`,
121532
+ ``,
121533
+ en ? `None. The plan matches the live account.` : `\u65E0\u3002\u8BA1\u5212\u4E0E\u8D26\u6237\u5B9E\u51B5\u4E00\u81F4\u3002`,
121534
+ ``
121535
+ );
121005
121536
  }
121006
121537
  lines.push(
121007
- `## \u8BF4\u660E`,
121538
+ en ? `## Notes` : `## \u8BF4\u660E`,
121008
121539
  ``,
121009
- result.ok ? `\u672C\u7CFB\u5217\u5DF2\u6309\u5E7F\u544A\u8BA1\u5212\u521B\u5EFA\u5B8C\u6210\u3002\u4E0A\u65B9\u8868\u683C\u5217\u51FA\u4E86\u6BCF\u4E00\u6761\u5173\u952E\u8BCD\u4E0E RSA \u6807\u9898/\u63CF\u8FF0\u7684\u751F\u6548\u72B6\u6001\uFF0C\u4FBF\u4E8E\u6838\u5BF9\u3002` : `\u672C\u7CFB\u5217\u5DF2\u90E8\u5206\u521B\u5EFA\u3002\u8BF7\u91CD\u70B9\u67E5\u770B\u300C\u4ECD\u672A\u521B\u5EFA\u9879\u300D\u4E0E\u8868\u683C\u4E2D\u7684 \u274C \u884C\uFF1B\u5982\u9700\u8865\u9F50\u7F3A\u5931\u5185\u5BB9\uFF0C\u53EF\u544A\u77E5\u52A9\u624B\u7EE7\u7EED\u5904\u7406\u3002`,
121540
+ result.ok ? en ? `This campaign matches the plan. The tables above show each keyword and RSA headline or description.` : `\u672C\u7CFB\u5217\u5DF2\u6309\u5E7F\u544A\u8BA1\u5212\u521B\u5EFA\u5B8C\u6210\u3002\u4E0A\u65B9\u8868\u683C\u5217\u51FA\u4E86\u6BCF\u4E00\u6761\u5173\u952E\u8BCD\u4E0E RSA \u6807\u9898/\u63CF\u8FF0\u7684\u751F\u6548\u72B6\u6001\uFF0C\u4FBF\u4E8E\u6838\u5BF9\u3002` : en ? `This campaign is only partly created. Check \u201CStill missing\u201D and the \u274C rows. Ask the assistant to fill the gaps.` : `\u672C\u7CFB\u5217\u5DF2\u90E8\u5206\u521B\u5EFA\u3002\u8BF7\u91CD\u70B9\u67E5\u770B\u300C\u4ECD\u672A\u521B\u5EFA\u9879\u300D\u4E0E\u8868\u683C\u4E2D\u7684 \u274C \u884C\uFF1B\u5982\u9700\u8865\u9F50\u7F3A\u5931\u5185\u5BB9\uFF0C\u53EF\u544A\u77E5\u52A9\u624B\u7EE7\u7EED\u5904\u7406\u3002`,
121010
121541
  ``
121011
121542
  );
121012
121543
  return lines.join("\n");
@@ -121025,8 +121556,8 @@ function resolveBatchDiffMdOutPath(opts) {
121025
121556
  }
121026
121557
  return path15.resolve(fileName);
121027
121558
  }
121028
- function writeCampaignCreateStatusMarkdown(result, mdPath) {
121029
- const reportMarkdown = buildCampaignCreateStatusMarkdown(result);
121559
+ function writeCampaignCreateStatusMarkdown(result, mdPath, lang = "zh") {
121560
+ const reportMarkdown = buildCampaignCreateStatusMarkdown(result, lang);
121030
121561
  fs10.mkdirSync(path15.dirname(mdPath), { recursive: true });
121031
121562
  fs10.writeFileSync(mdPath, reportMarkdown, "utf8");
121032
121563
  return { reportMarkdown, reportMarkdownFile: mdPath };
@@ -121194,7 +121725,7 @@ async function runCampaignBatchDiff(opts) {
121194
121725
  jsonOut: opts.jsonOut,
121195
121726
  campaignId
121196
121727
  });
121197
- const report = writeCampaignCreateStatusMarkdown(result, mdPath);
121728
+ const report = writeCampaignCreateStatusMarkdown(result, mdPath, opts.lang === "en" ? "en" : "zh");
121198
121729
  const payload = {
121199
121730
  ...result,
121200
121731
  ...report
@@ -124880,6 +125411,9 @@ PMax \u5730\u7406\u62A5\u8868\uFF08\u6D3B\u52A8\xD7\u56FD\u5BB6\uFF0C${query.get
124880
125411
  });
124881
125412
  }
124882
125413
 
125414
+ // src/commands/ad/_register.ts
125415
+ init_lang();
125416
+
124883
125417
  // src/commands/ad/campaign-validate.ts
124884
125418
  import { writeFileSync as writeFileSync4 } from "fs";
124885
125419
  init_cli_json_snapshot();
@@ -125248,7 +125782,7 @@ function register21(program2) {
125248
125782
  ).option("--batch-id <id>", "\u6279\u91CF\u4EFB\u52A1 ID\uFF08\u6821\u9A8C\u975E Failed\uFF1BHasFailed \u65F6\u53EF\u7701\u7565 campaign-id\uFF09").option("-a, --account <id>", "\u5A92\u4F53\u8D26\u6237 ID\uFF08\u9ED8\u8BA4\u8BFB JSON \u7684 account\uFF09").option("--json-out <path>", "\u6BD4\u5BF9\u7ED3\u679C\u843D\u76D8\u8DEF\u5F84\uFF08\u542B reportMarkdown\uFF09").option(
125249
125783
  "--md-out <path>",
125250
125784
  "\u521B\u5EFA\u8BE6\u60C5 Markdown \u843D\u76D8\u8DEF\u5F84\uFF08\u9ED8\u8BA4\uFF1A\u4E0E --json-out \u540C\u76EE\u5F55\u7684 campaign-create-status-<campaignId>.md\uFF09"
125251
- ).option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
125785
+ ).option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).option("--lang <lang>", "\u4EA4\u7ED9\u7528\u6237\u7684\u521B\u5EFA\u8BE6\u60C5\u8BED\u8A00\uFF1Azh \u6216 en\uFF0C\u9ED8\u8BA4 zh").action(
125252
125786
  async (opts) => {
125253
125787
  await runCampaignBatchDiff({
125254
125788
  token: opts.token,
@@ -125258,7 +125792,8 @@ function register21(program2) {
125258
125792
  account: opts.account,
125259
125793
  jsonOut: opts.jsonOut,
125260
125794
  mdOut: opts.mdOut,
125261
- verbose: opts.verbose
125795
+ verbose: opts.verbose,
125796
+ lang: readReportLangOrExit({ flag: opts.lang })
125262
125797
  });
125263
125798
  }
125264
125799
  );
@@ -128015,6 +128550,8 @@ async function runMetaAdFormCreate(opts) {
128015
128550
  init_xlsx_style_engine();
128016
128551
  import fs12 from "fs";
128017
128552
  import path17 from "path";
128553
+ init_html();
128554
+ init_lang();
128018
128555
  var PENDING = "[\u5F85\u8865]";
128019
128556
  var SHEET_NAMES = ["\u65B9\u6848\u603B\u89C8", "1-\u5BA2\u6237\u753B\u50CF", "2-\u8D26\u6237\u7ED3\u6784", "3-\u8868\u5355\u8BE2\u76D8C1"];
128020
128557
  var OPS = {
@@ -128035,6 +128572,9 @@ var QUESTION_TYPE_LABEL = {
128035
128572
  JOB_TITLE: "Job Title",
128036
128573
  CUSTOM: "Custom"
128037
128574
  };
128575
+ function chrome(text3, lang) {
128576
+ return lang === "en" ? applyEnglishPhrases(text3) : text3;
128577
+ }
128038
128578
  function text2(value) {
128039
128579
  if (value == null) return PENDING;
128040
128580
  const s = String(value).trim();
@@ -128149,12 +128689,13 @@ function resolveCopyBlocks(cfg) {
128149
128689
  }
128150
128690
  return blocks;
128151
128691
  }
128152
- function writeOpsExcel(outPath, cfg) {
128692
+ function writeOpsExcel(outPath, cfg, lang = "zh") {
128153
128693
  const plan = cfg.plan ?? {};
128154
128694
  const wb = createExcelWorkbook({ accent: "1F4E79" });
128155
128695
  const adSets = resolveAdSets(cfg);
128156
128696
  const subtitle = defaultSubtitle(cfg);
128157
- const overview = wb.addSheet(SHEET_NAMES[0]);
128697
+ const sheet = (index) => chrome(SHEET_NAMES[index], lang);
128698
+ const overview = wb.addSheet(sheet(0));
128158
128699
  overview.setColWidths([20, 18, 18, 18, 18, 42]);
128159
128700
  overview.titleBar(overviewTitle(plan), {
128160
128701
  fill: null,
@@ -128163,22 +128704,22 @@ function writeOpsExcel(outPath, cfg) {
128163
128704
  hAlign: "center"
128164
128705
  });
128165
128706
  overview.blankRow();
128166
- overview.kvRow("\u5BA2\u6237\u540D\u79F0", text2(plan.customerName));
128167
- overview.kvRow("Facebook \u4E3B\u9875", text2(plan.pageUrl || "\u5F85 pages \u540E\u8865"), {
128707
+ overview.kvRow(chrome("\u5BA2\u6237\u540D\u79F0", lang), text2(plan.customerName));
128708
+ overview.kvRow(chrome("Facebook \u4E3B\u9875", lang), text2(plan.pageUrl || chrome("\u5F85 pages \u540E\u8865", lang)), {
128168
128709
  hyperlink: plan.pageUrl?.startsWith("http") ? plan.pageUrl : void 0
128169
128710
  });
128170
- overview.kvRow("\u5B98\u65B9\u7F51\u7AD9", text2(plan.siteUrl), {
128711
+ overview.kvRow(chrome("\u5B98\u65B9\u7F51\u7AD9", lang), text2(plan.siteUrl), {
128171
128712
  hyperlink: plan.siteUrl?.startsWith("http") ? plan.siteUrl : void 0
128172
128713
  });
128173
- overview.kvRow("\u4E3B\u8425\u4E1A\u52A1", text2(plan.businessOneLiner));
128174
- overview.kvRow("\u6838\u5FC3\u4EA7\u54C1", text2(plan.products));
128175
- overview.kvRow("\u6838\u5FC3\u5356\u70B9", text2(plan.sellingPoints));
128176
- overview.kvRow("\u76EE\u6807\u5E02\u573A", text2(plan.targetMarkets || joinList(cfg.adset?.countries, ", ")));
128177
- overview.kvRow("\u8425\u9500\u76EE\u6807", text2(plan.mainResult || "\u8868\u5355\u8BE2\u76D8\uFF08Lead Form\uFF09"));
128178
- overview.kvRow("\u65E5\u9884\u7B97", dailyBudgetText(cfg));
128179
- overview.kvRow("\u5E7F\u544A\u7ED3\u6784", structureSummary(cfg));
128180
- overview.kvRow("\u6295\u653E\u7B56\u7565", text2(plan.strategy || plan.landingType));
128181
- const profile = wb.addSheet(SHEET_NAMES[1]);
128714
+ overview.kvRow(chrome("\u4E3B\u8425\u4E1A\u52A1", lang), text2(plan.businessOneLiner));
128715
+ overview.kvRow(chrome("\u6838\u5FC3\u4EA7\u54C1", lang), text2(plan.products));
128716
+ overview.kvRow(chrome("\u6838\u5FC3\u5356\u70B9", lang), text2(plan.sellingPoints));
128717
+ overview.kvRow(chrome("\u76EE\u6807\u5E02\u573A", lang), text2(plan.targetMarkets || joinList(cfg.adset?.countries, ", ")));
128718
+ overview.kvRow(chrome("\u8425\u9500\u76EE\u6807", lang), text2(plan.mainResult || chrome("\u8868\u5355\u8BE2\u76D8\uFF08Lead Form\uFF09", lang)));
128719
+ overview.kvRow(chrome("\u65E5\u9884\u7B97", lang), dailyBudgetText(cfg));
128720
+ overview.kvRow(chrome("\u5E7F\u544A\u7ED3\u6784", lang), structureSummary(cfg));
128721
+ overview.kvRow(chrome("\u6295\u653E\u7B56\u7565", lang), text2(plan.strategy || plan.landingType));
128722
+ const profile = wb.addSheet(sheet(1));
128182
128723
  profile.setColWidths([26, 48, 26, 26]);
128183
128724
  profile.titleBar(profileTitle(plan), { fill: OPS.title });
128184
128725
  profile.subtitleBar(subtitle);
@@ -128220,7 +128761,7 @@ function writeOpsExcel(outPath, cfg) {
128220
128761
  profile.noteRow(
128221
128762
  plan.targeting?.disclaimer?.trim() || "\u672C\u6279\u7EC4\u5B9A\u5411\u53EA\u8BA4\u56FD\u5BB6/\u5E74\u9F84/\u6027\u522B\uFF0C\u5174\u8DA3\u8BCD\u4EC5\u4F5C\u7D20\u6750\u4E0E\u83B7\u5BA2\u8BF4\u660E"
128222
128763
  );
128223
- const structure = wb.addSheet(SHEET_NAMES[2]);
128764
+ const structure = wb.addSheet(sheet(2));
128224
128765
  structure.setColWidths([18, 44, 26, 22]);
128225
128766
  structure.titleBar(text2(plan.brand ? `\u5E7F\u544A\u8D26\u6237\u7ED3\u6784\uFF1A${plan.brand}` : "\u5E7F\u544A\u8D26\u6237\u7ED3\u6784"), {
128226
128767
  fill: OPS.title
@@ -128259,7 +128800,7 @@ function writeOpsExcel(outPath, cfg) {
128259
128800
  structure.noteRow(
128260
128801
  plan.structureNote?.trim() || `\u8BF4\u660E\uFF1AC1 \u8868\u5355\u8BE2\u76D8\u8BA1\u5212 ${adSets.length} \u7EC4\uFF1B\u672C\u6B21 create \u53EA\u843D\u5730 1 \u7CFB\u5217 \xD7 1 \u7EC4 \xD7 1 \u521B\u610F \xD7 1 \u5E7F\u544A\uFF0C\u5176\u4F59\u786E\u8BA4\u540E\u7528\u539F\u8BED\u7EED\u5EFA\u3002`
128261
128802
  );
128262
- const lead = wb.addSheet(SHEET_NAMES[3]);
128803
+ const lead = wb.addSheet(sheet(3));
128263
128804
  lead.setColWidths([16, 22, 36, 40, 16, 18]);
128264
128805
  lead.titleBar(`Campaign 1\uFF1A\u8868\u5355\u8BE2\u76D8\uFF08Leads\uFF09\uFF5C ${dailyBudgetText(cfg)}`, { fill: OPS.title });
128265
128806
  lead.subtitleBar(subtitle);
@@ -128313,7 +128854,7 @@ function writeOpsExcel(outPath, cfg) {
128313
128854
  }
128314
128855
  return wb.writeFile(outPath);
128315
128856
  }
128316
- function renderMarkdown(cfg) {
128857
+ function renderMarkdown(cfg, lang = "zh") {
128317
128858
  const plan = cfg.plan ?? {};
128318
128859
  const adSets = resolveAdSets(cfg);
128319
128860
  const questions = (cfg.form?.questions ?? []).map(
@@ -128325,13 +128866,14 @@ function renderMarkdown(cfg) {
128325
128866
 
128326
128867
  ${text2(block.text)}
128327
128868
  `).join("\n");
128869
+ const h = (label2) => chrome(label2, lang);
128328
128870
  return `# ${overviewTitle(plan)}
128329
128871
 
128330
- - \u5BA2\u6237/\u7AD9\u70B9\uFF1A${text2(plan.customerName)} + ${text2(plan.siteUrl)} + ${text2(plan.businessOneLiner)}
128331
- - \u4E3B\u7ED3\u679C\uFF1A${text2(plan.mainResult)}
128872
+ - ${h("\u5BA2\u6237\u540D\u79F0")}/${h("\u5B98\u65B9\u7F51\u7AD9")}\uFF1A${text2(plan.customerName)} + ${text2(plan.siteUrl)} + ${text2(plan.businessOneLiner)}
128873
+ - ${h("\u8425\u9500\u76EE\u6807")}\uFF1A${text2(plan.mainResult)}
128332
128874
  - \u843D\u5730\u5EFA\u8BAE\uFF1A${text2(plan.landingType)}
128333
- - \u65E5\u9884\u7B97\uFF1A${dailyBudgetText(cfg)}\u3000\u56FD\u5BB6\uFF1A${joinList(cfg.adset?.countries, ", ")}
128334
- - \u5E7F\u544A\u7ED3\u6784\uFF1A${structureSummary(cfg)}
128875
+ - ${h("\u65E5\u9884\u7B97")}\uFF1A${dailyBudgetText(cfg)}\u3000${h("\u56FD\u5BB6")}\uFF1A${joinList(cfg.adset?.countries, ", ")}
128876
+ - ${h("\u5E7F\u544A\u7ED3\u6784")}\uFF1A${structureSummary(cfg)}
128335
128877
  - \u8BA1\u5212\u72B6\u6001\uFF1A**\u5F85\u786E\u8BA4**\u3000\u672C\u6279\u53EF\u6267\u884C\uFF1AInstant Form \u7EBF\u7D22\u5E7F\u544A\uFF08\u975E Advantage+\uFF09
128336
128878
 
128337
128879
  > \u8D26\u6237\uFF1A${text2(cfg.account)}\u3000\u4E3B\u9875\uFF1A${text2(cfg.pageId)}
@@ -128391,24 +128933,25 @@ ${copies}
128391
128933
  \u8349\u6848\u4F9D\u636E\u4E3A\u516C\u5F00\u9875\u9762/\u5BA2\u6237\u6750\u6599\uFF0C\u975E\u5E7F\u544A\u540E\u53F0\u6570\u636E\u3002
128392
128934
  `;
128393
128935
  }
128394
- function writeMetaLeadPlanExcel(outPath, cfg) {
128395
- return writeOpsExcel(outPath, cfg);
128936
+ function writeMetaLeadPlanExcel(outPath, cfg, lang = "zh") {
128937
+ return writeOpsExcel(outPath, cfg, lang);
128396
128938
  }
128397
- function writeMetaLeadPlanMarkdown(outPath, cfg) {
128939
+ function writeMetaLeadPlanMarkdown(outPath, cfg, lang = "zh") {
128398
128940
  const abs = path17.resolve(outPath);
128399
128941
  fs12.mkdirSync(path17.dirname(abs), { recursive: true });
128400
- fs12.writeFileSync(abs, renderMarkdown(cfg), "utf8");
128942
+ fs12.writeFileSync(abs, renderMarkdown(cfg, lang), "utf8");
128401
128943
  return abs;
128402
128944
  }
128403
128945
  async function runMetaAdPlanRender(opts) {
128404
128946
  const cfg = loadMetaLeadCreateConfig(opts.configFile);
128947
+ const lang = readReportLangOrExit({ flag: opts.lang });
128405
128948
  const configDir = path17.dirname(path17.resolve(opts.configFile));
128406
128949
  const xlsxOut = path17.resolve(opts.out || path17.join(configDir, "meta-lead-plan.xlsx"));
128407
128950
  const mdOut = path17.resolve(
128408
128951
  opts.mdOut || path17.join(path17.dirname(xlsxOut), `${path17.parse(xlsxOut).name}.md`)
128409
128952
  );
128410
- const xlsxPath = writeMetaLeadPlanExcel(xlsxOut, cfg);
128411
- const mdPath = writeMetaLeadPlanMarkdown(mdOut, cfg);
128953
+ const xlsxPath = writeMetaLeadPlanExcel(xlsxOut, cfg, lang);
128954
+ const mdPath = writeMetaLeadPlanMarkdown(mdOut, cfg, lang);
128412
128955
  console.log(
128413
128956
  JSON.stringify({
128414
128957
  ok: true,
@@ -129196,7 +129739,7 @@ function register22(program2) {
129196
129739
  await runMetaAdCreate(opts);
129197
129740
  }
129198
129741
  );
129199
- meta.command("plan-render").description("\u6309\u8FD0\u8425\u56FA\u5B9A 4 Sheet \u6295\u5F71 Facebook \u7EBF\u7D22\u65B9\u6848 Excel + Markdown\uFF08\u4E0D\u8C03\u7F51\u5173\uFF1B\u7248\u5F0F\u9501\u6B7B\uFF09").requiredOption("--config-file <path>", "meta-lead-create JSON").option("--out <file>", "xlsx \u8F93\u51FA\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u4E0E JSON \u540C\u76EE\u5F55 meta-lead-plan.xlsx\uFF09").option("--md-out <file>", "Markdown \u5BA1\u67E5\u7A3F\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u4E0E xlsx \u540C\u540D .md\uFF09").action(async (opts) => {
129742
+ meta.command("plan-render").description("\u6309\u8FD0\u8425\u56FA\u5B9A 4 Sheet \u6295\u5F71 Facebook \u7EBF\u7D22\u65B9\u6848 Excel + Markdown\uFF08\u4E0D\u8C03\u7F51\u5173\uFF1B\u7248\u5F0F\u9501\u6B7B\uFF09").requiredOption("--config-file <path>", "meta-lead-create JSON").option("--out <file>", "xlsx \u8F93\u51FA\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u4E0E JSON \u540C\u76EE\u5F55 meta-lead-plan.xlsx\uFF09").option("--md-out <file>", "Markdown \u5BA1\u67E5\u7A3F\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u4E0E xlsx \u540C\u540D .md\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u9ED8\u8BA4\u4E2D\u6587").action(async (opts) => {
129200
129743
  await runMetaAdPlanRender(opts);
129201
129744
  });
129202
129745
  addCommon(addAccount(meta.command("account").description("\u8BFB\u5E7F\u544A\u8D26\u6237\uFF08\u4F59\u989D/\u82B1\u8D39/\u5E01\u79CD\uFF09"))).action(
@@ -134470,6 +135013,8 @@ async function runWebsiteDiagnosisPrepare(opts) {
134470
135013
  }
134471
135014
 
134472
135015
  // src/commands/website-diagnosis/render-report.ts
135016
+ init_lang();
135017
+ init_html();
134473
135018
  import fs16 from "fs";
134474
135019
  import fsPromises from "fs/promises";
134475
135020
  import path21 from "path";
@@ -134521,13 +135066,14 @@ function mergeCollectLighthouse(data, collect) {
134521
135066
  ...collect.lighthouseWarning && !data.lighthouseWarning ? { lighthouseWarning: collect.lighthouseWarning } : {}
134522
135067
  };
134523
135068
  }
134524
- function injectReportData(html, payload) {
134525
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
135069
+ function injectReportData(html, payload, lang) {
135070
+ const localized = localizeReportHtml(html, lang);
135071
+ const json = JSON.stringify(stampMetaLang(payload, lang)).replace(/</g, "\\u003c");
134526
135072
  const marker = "window.__WEBSITE_DIAGNOSIS__ = window.__WEBSITE_DIAGNOSIS__ || null;";
134527
- if (!html.includes(marker)) {
135073
+ if (!localized.includes(marker)) {
134528
135074
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 website-diagnosis-report.html");
134529
135075
  }
134530
- return html.replace(marker, `window.__WEBSITE_DIAGNOSIS__ = ${json};`);
135076
+ return localized.replace(marker, `window.__WEBSITE_DIAGNOSIS__ = ${json};`);
134531
135077
  }
134532
135078
  async function runWebsiteDiagnosisRender(opts) {
134533
135079
  const dataPath = path21.resolve(opts.dataFile);
@@ -134537,6 +135083,7 @@ async function runWebsiteDiagnosisRender(opts) {
134537
135083
  const collectRaw = await readJsonFile(path21.resolve(opts.collectFile));
134538
135084
  data = mergeCollectLighthouse(data, asRecord5(collectRaw));
134539
135085
  }
135086
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
134540
135087
  const templatePath = websiteDiagnosisReportTemplatePath();
134541
135088
  let html;
134542
135089
  try {
@@ -134553,7 +135100,7 @@ async function runWebsiteDiagnosisRender(opts) {
134553
135100
  );
134554
135101
  const outDir = path21.dirname(outPath);
134555
135102
  await fsPromises.mkdir(outDir, { recursive: true });
134556
- await fsPromises.writeFile(outPath, injectReportData(html, data), "utf8");
135103
+ await fsPromises.writeFile(outPath, injectReportData(html, data, lang), "utf8");
134557
135104
  const runtimeSrc = websiteDiagnosisRuntimePath();
134558
135105
  const runtimeOut = path21.join(outDir, TEMPLATE_BASENAMES.runtime);
134559
135106
  let runtimeCopied = false;
@@ -134620,11 +135167,12 @@ function registerWebsiteDiagnosisCommands(program2) {
134620
135167
  jsonOut: opts.jsonOut
134621
135168
  });
134622
135169
  });
134623
- root.command("render").description("\u6839\u636E\u8BCA\u65AD JSON \u751F\u6210 HTML \u7EC8\u7A3F\uFF08\u9ED8\u8BA4\u4EA4\u4ED8\u683C\u5F0F\uFF0Cwebsite-diagnosis-report.html\uFF09").requiredOption("--data <file>", "\u8BCA\u65AD\u7ED3\u679C JSON\uFF08getWebsiteDiagnosisData \u540C\u7ED3\u6784\uFF09").option("--collect <file>", "\u53EF\u9009\uFF1Acollect \u843D\u76D8 JSON\uFF0C\u7528\u4E8E\u5408\u5E76 lighthouse").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
135170
+ root.command("render").description("\u6839\u636E\u8BCA\u65AD JSON \u751F\u6210 HTML \u7EC8\u7A3F\uFF08\u9ED8\u8BA4\u4EA4\u4ED8\u683C\u5F0F\uFF0Cwebsite-diagnosis-report.html\uFF09").requiredOption("--data <file>", "\u8BCA\u65AD\u7ED3\u679C JSON\uFF08getWebsiteDiagnosisData \u540C\u7ED3\u6784\uFF09").option("--collect <file>", "\u53EF\u9009\uFF1Acollect \u843D\u76D8 JSON\uFF0C\u7528\u4E8E\u5408\u5E76 lighthouse").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(async (opts) => {
134624
135171
  await runWebsiteDiagnosisRender({
134625
135172
  dataFile: opts.data,
134626
135173
  collectFile: opts.collect,
134627
- out: opts.out
135174
+ out: opts.out,
135175
+ lang: opts.lang
134628
135176
  });
134629
135177
  });
134630
135178
  }
@@ -134980,12 +135528,12 @@ function countTableColumns(tableHtml) {
134980
135528
  return Math.max(th, td);
134981
135529
  }
134982
135530
  function isCompareTable(tableHtml, html, tableIndex) {
134983
- const inTable = /对比|竞品|功能|性能|维度|指标|参数/.test(tableHtml);
135531
+ const inTable = /对比|竞品|功能|性能|维度|指标|参数|competitor|compare|feature/i.test(tableHtml);
134984
135532
  if (inTable) return true;
134985
135533
  const windowStart = Math.max(0, tableIndex - 600);
134986
135534
  const windowEnd = Math.min(html.length, tableIndex + tableHtml.length + 400);
134987
135535
  const nearby = html.slice(windowStart, windowEnd);
134988
- return /对比|竞品|竞争|功能|性能|对标/.test(nearby);
135536
+ return /对比|竞品|竞争|功能|性能|对标|competitor|compare|benchmark/i.test(nearby);
134989
135537
  }
134990
135538
  function findFeatureCompareTableRows(html) {
134991
135539
  const tables = html.match(/<table[\s\S]*?<\/table>/gi) ?? [];
@@ -135002,9 +135550,9 @@ function findFeatureCompareTableRows(html) {
135002
135550
  return best;
135003
135551
  }
135004
135552
  function countPainPoints(html) {
135005
- const labeled = (html.match(/痛点\s*\d/gi) ?? []).length;
135553
+ const labeled = (html.match(/(?:痛点|pain\s*point)\s*\d/gi) ?? []).length;
135006
135554
  if (labeled >= 4) return labeled;
135007
- const section = html.match(/痛点[\s\S]{0,3000}/i)?.[0] ?? html;
135555
+ const section = html.match(/(?:痛点|pain points?)[\s\S]{0,3000}/i)?.[0] ?? html;
135008
135556
  const listItems = (section.match(/<li[^>]*>/gi) ?? []).length;
135009
135557
  return Math.max(labeled, listItems >= 4 ? 4 : listItems);
135010
135558
  }
@@ -135020,7 +135568,7 @@ function hasEchartsInlineResize(html) {
135020
135568
  return /addEventListener\s*\(\s*['"]resize['"]/.test(html) && /\.resize\s*\(/.test(html);
135021
135569
  }
135022
135570
  function countCompetitorMentions(html) {
135023
- const competitorSection = html.match(/(?:主要)?竞品[\s\S]{0,5000}/i)?.[0] ?? html.match(/竞争对手[\s\S]{0,5000}/i)?.[0] ?? html.match(/对标(?:企业|品牌|公司)[\s\S]{0,5000}/i)?.[0] ?? "";
135571
+ const competitorSection = html.match(/(?:主要)?竞品[\s\S]{0,5000}/i)?.[0] ?? html.match(/竞争对手[\s\S]{0,5000}/i)?.[0] ?? html.match(/对标(?:企业|品牌|公司)[\s\S]{0,5000}/i)?.[0] ?? html.match(/competitors?[\s\S]{0,5000}/i)?.[0] ?? "";
135024
135572
  if (competitorSection) {
135025
135573
  const listItems = (competitorSection.match(/<li[^>]*>[\s\S]*?<\/li>/gi) ?? []).filter(
135026
135574
  (li) => stripHtmlTags(li).trim().length >= 2
@@ -135061,8 +135609,10 @@ function splitMarketTokens(value) {
135061
135609
  return value.split(/[、,,/|]/).map((t) => t.trim()).filter((t) => t.length >= 2);
135062
135610
  }
135063
135611
  function hasTargetGeoBreakdown(html, ctx) {
135064
- const hasMetrics = /规模|增长|优先级|潜力|份额|CAGR|市场容量/.test(html);
135065
- const hasGeoFrame = /国家|区域|地区|市场/.test(html);
135612
+ const hasMetrics = /规模|增长|优先级|潜力|份额|CAGR|市场容量|market size|growth|priority|share/i.test(
135613
+ html
135614
+ );
135615
+ const hasGeoFrame = /国家|区域|地区|市场|country|region|market/i.test(html);
135066
135616
  if (!hasMetrics || !hasGeoFrame) return false;
135067
135617
  const targetMarket = ctx.targetMarket?.trim() ?? "";
135068
135618
  if (targetMarket) {
@@ -135079,56 +135629,58 @@ function hasTargetGeoBreakdown(html, ctx) {
135079
135629
  return regionHits >= 2;
135080
135630
  }
135081
135631
  function hasReportTitle(html, ctx) {
135082
- const hasTitle = /战略市场分析|KA客户|行业分析报告|市场分析报告|行业调查|市场调查/.test(html);
135632
+ const hasTitle = /战略市场分析|KA客户|行业分析报告|市场分析报告|行业调查|市场调查|industry analysis|market analysis|strategic market/i.test(
135633
+ html
135634
+ );
135083
135635
  if (!hasTitle) return false;
135084
135636
  return /目标市场|全球市场|行业|细分|market/i.test(html) || !!ctx.targetMarket?.trim() || !!ctx.industry?.trim() || !!ctx.customerName?.trim();
135085
135637
  }
135086
135638
  var CONTENT_RULES = [
135087
135639
  { id: "cover", test: (h, ctx) => hasReportTitle(h, ctx) },
135088
135640
  { id: "cover-kpi", test: (h) => /核心结论|metric|kpi|结论|摘要|executive summary/i.test(h) },
135089
- { id: "ch1", test: (h) => /市场与趋势诊断/.test(h) },
135641
+ { id: "ch1", test: (h) => /市场与趋势诊断|market and trend/i.test(h) },
135090
135642
  {
135091
135643
  id: "ch1-tam",
135092
- test: (h) => (/TAM|SAM|SOM/.test(h) || /市场规模/.test(h)) && /CAGR|增长率|增长趋势/.test(h)
135644
+ test: (h) => (/TAM|SAM|SOM/.test(h) || /市场规模|market size/i.test(h)) && /CAGR|增长率|增长趋势|growth/i.test(h)
135093
135645
  },
135094
- { id: "ch1-value-chain", test: (h) => /价值链|生态定位|产业链/.test(h) },
135646
+ { id: "ch1-value-chain", test: (h) => /价值链|生态定位|产业链|value chain|ecosystem/i.test(h) },
135095
135647
  { id: "ch1-target-geo", test: (h, ctx) => hasTargetGeoBreakdown(h, ctx) },
135096
- { id: "ch1-pestel", test: (h) => /PESTEL|政策|法规环境|技术趋势|经济|社会|环境/.test(h) },
135097
- { id: "ch1-forecast", test: (h) => /预测|3.?5年|未来\d/.test(h) },
135098
- { id: "ch2", test: (h) => /行业深度洞察|目标行业/.test(h) },
135648
+ { id: "ch1-pestel", test: (h) => /PESTEL|政策|法规环境|技术趋势|经济|社会|环境|policy|regulatory/i.test(h) },
135649
+ { id: "ch1-forecast", test: (h) => /预测|3.?5年|未来\d|forecast|next \d/i.test(h) },
135650
+ { id: "ch2", test: (h) => /行业深度洞察|目标行业|industry insight|target industry/i.test(h) },
135099
135651
  { id: "ch2-pain", test: (h, ctx) => ctx.painPointCount >= 4 },
135100
- { id: "ch2-compliance", test: (h) => /法规|合规/.test(h) && /<table/.test(h) },
135652
+ { id: "ch2-compliance", test: (h) => /法规|合规|regulation|compliance/i.test(h) && /<table/.test(h) },
135101
135653
  {
135102
135654
  id: "ch2-bpmn",
135103
- test: (h) => /原流程|BPMN|介入后|新流程/.test(h) && /效率|成本|提升|下降|节约/.test(h)
135655
+ test: (h) => /原流程|BPMN|介入后|新流程|as-is|to-be|current process/i.test(h) && /效率|成本|提升|下降|节约|efficiency|cost|improve/i.test(h)
135104
135656
  },
135105
- { id: "ch3", test: (h) => /目标受众|受众与场景/.test(h) },
135106
- { id: "ch3-audience", test: (h) => /用户|受众|画像|群体/.test(h) && /<table/.test(h) },
135107
- { id: "ch3-scenario", test: (h) => /应用场景|场景|差异化|用户价值|企业价值/.test(h) },
135108
- { id: "ch4", test: (h) => /竞争策略|竞争.{0,6}定位/.test(h) },
135657
+ { id: "ch3", test: (h) => /目标受众|受众与场景|target audience|audience and scenario/i.test(h) },
135658
+ { id: "ch3-audience", test: (h) => /用户|受众|画像|群体|audience|persona|segment/i.test(h) && /<table/.test(h) },
135659
+ { id: "ch3-scenario", test: (h) => /应用场景|场景|差异化|用户价值|企业价值|use case|scenario/i.test(h) },
135660
+ { id: "ch4", test: (h) => /竞争策略|竞争.{0,6}定位|competitive (strategy|positioning)/i.test(h) },
135109
135661
  { id: "ch4-competitors", test: (h, ctx) => ctx.competitorCount >= 3 },
135110
135662
  { id: "ch4-compare-table", test: (h, ctx) => ctx.competitorTableRows >= 11 },
135111
135663
  {
135112
135664
  id: "ch4-gaps",
135113
135665
  test: (h) => [
135114
- /功能空白|功能.*空白/.test(h),
135115
- /场景/.test(h),
135116
- /用户群|用户/.test(h),
135117
- /地域|地区|区域/.test(h)
135666
+ /功能空白|功能.*空白|feature gap/i.test(h),
135667
+ /场景|scenario/i.test(h),
135668
+ /用户群|用户|audience|segment/i.test(h),
135669
+ /地域|地区|区域|geo|region/i.test(h)
135118
135670
  ].filter(Boolean).length >= 3
135119
135671
  },
135120
- { id: "ch4-positioning", test: (h) => /定位陈述|定位:|核心定位|差异化/.test(h) },
135121
- { id: "ch5", test: (h) => /增长与品牌|GTM/.test(h) },
135122
- { id: "ch5-gtm", test: (h) => /试点|规模复制|生态扩展|三阶段/.test(h) && /KPI|ROI|指标/.test(h) },
135123
- { id: "ch5-brand", test: (h) => /品牌|传播|主张/.test(h) },
135124
- { id: "ch5-tactics", test: (h) => /内容营销|渠道|KOL|销售模式|营销/.test(h) },
135125
- { id: "ch6", test: (h) => /实施/.test(h) && /风险|资源/.test(h) },
135126
- { id: "ch6-milestone", test: (h) => /里程碑|阶段/.test(h) && /任务|负责人|成果|指标/.test(h) },
135127
- { id: "ch6-budget", test: (h) => /预算|资源需求|人力|分配/.test(h) },
135128
- { id: "ch6-risk", test: (h) => /风险/.test(h) && /应对|措施|矩阵/.test(h) },
135129
- { id: "appendix", test: (h) => /附录|数据来源/.test(h) },
135130
- { id: "appendix-sources", test: (h) => /数据来源|Source:|报告名称|发布日期|http/.test(h) },
135131
- { id: "appendix-methods", test: (h) => /调研方法|桌面研究|访谈|问卷/.test(h) },
135672
+ { id: "ch4-positioning", test: (h) => /定位陈述|定位:|核心定位|差异化|positioning|differentiat/i.test(h) },
135673
+ { id: "ch5", test: (h) => /增长与品牌|GTM|growth and brand/i.test(h) },
135674
+ { id: "ch5-gtm", test: (h) => /试点|规模复制|生态扩展|三阶段|pilot|scale|three phases/i.test(h) && /KPI|ROI|指标|metric/i.test(h) },
135675
+ { id: "ch5-brand", test: (h) => /品牌|传播|主张|brand|message/i.test(h) },
135676
+ { id: "ch5-tactics", test: (h) => /内容营销|渠道|KOL|销售模式|营销|content marketing|channel/i.test(h) },
135677
+ { id: "ch6", test: (h) => /实施|implementation/i.test(h) && /风险|资源|risk|resource/i.test(h) },
135678
+ { id: "ch6-milestone", test: (h) => /里程碑|阶段|milestone|phase/i.test(h) && /任务|负责人|成果|指标|owner|deliverable/i.test(h) },
135679
+ { id: "ch6-budget", test: (h) => /预算|资源需求|人力|分配|budget|headcount/i.test(h) },
135680
+ { id: "ch6-risk", test: (h) => /风险|risk/i.test(h) && /应对|措施|矩阵|mitigation|matrix/i.test(h) },
135681
+ { id: "appendix", test: (h) => /附录|数据来源|appendix|sources/i.test(h) },
135682
+ { id: "appendix-sources", test: (h) => /数据来源|Source:|报告名称|发布日期|http/i.test(h) },
135683
+ { id: "appendix-methods", test: (h) => /调研方法|桌面研究|访谈|问卷|desk research|interview|survey/i.test(h) },
135132
135684
  {
135133
135685
  id: "appendix-glossary",
135134
135686
  test: (h) => [/ROI/.test(h), /CAGR/.test(h), /GTM/.test(h), /BPMN/.test(h)].filter(Boolean).length >= 3
@@ -135193,6 +135745,7 @@ function formatMarketReportContentErrors(result) {
135193
135745
  }
135194
135746
 
135195
135747
  // src/commands/market-analysis/render-report.ts
135748
+ init_lang();
135196
135749
  function asRecord6(value) {
135197
135750
  if (value && typeof value === "object" && !Array.isArray(value)) {
135198
135751
  return value;
@@ -135236,6 +135789,16 @@ async function runMarketAnalysisRender(opts) {
135236
135789
  process.exit(1);
135237
135790
  }
135238
135791
  const reportData = asRecord6(dataRaw);
135792
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: reportData.meta });
135793
+ try {
135794
+ assertHtmlLangMatches(html, lang);
135795
+ } catch (err) {
135796
+ const message = err instanceof Error ? err.message : String(err);
135797
+ console.error(`
135798
+ \u274C ${message}
135799
+ `);
135800
+ process.exit(1);
135801
+ }
135239
135802
  const contentCheck = validateMarketReportContent(html, validationContextFromReport(reportData));
135240
135803
  if (!contentCheck.ok) {
135241
135804
  console.error(`
@@ -135287,10 +135850,11 @@ function registerMarketAnalysisCommands(program2) {
135287
135850
  });
135288
135851
  }
135289
135852
  );
135290
- root.command("render").description("\u6839\u636E Agent \u751F\u6210\u7684 market-report.json\uFF08\u542B htmlContent\uFF09\u5199\u51FA HTML \u7EC8\u7A3F").requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 market-report.json").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
135853
+ root.command("render").description("\u6839\u636E Agent \u751F\u6210\u7684 market-report.json\uFF08\u542B htmlContent\uFF09\u5199\u51FA HTML \u7EC8\u7A3F").requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 market-report.json").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u987B\u4E0E html lang \u4E00\u81F4").action(async (opts) => {
135291
135854
  await runMarketAnalysisRender({
135292
135855
  dataFile: opts.data,
135293
- out: opts.out
135856
+ out: opts.out,
135857
+ lang: opts.lang
135294
135858
  });
135295
135859
  });
135296
135860
  }
@@ -137467,6 +138031,8 @@ function applyDataRenderedCopy(data) {
137467
138031
  }
137468
138032
 
137469
138033
  // src/commands/google-ads-diagnosis/render-report.ts
138034
+ init_lang();
138035
+ init_html();
137470
138036
  function asRecord16(value) {
137471
138037
  if (value && typeof value === "object" && !Array.isArray(value)) {
137472
138038
  return value;
@@ -137590,9 +138156,10 @@ async function runGoogleAdsDiagnosisRender(opts) {
137590
138156
  `);
137591
138157
  process.exit(1);
137592
138158
  }
138159
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
137593
138160
  let rendered;
137594
138161
  try {
137595
- rendered = injectGoogleAdsDiagnosisData(html, data);
138162
+ rendered = injectGoogleAdsDiagnosisData(localizeReportHtml(html, lang), stampMetaLang(data, lang));
137596
138163
  } catch (err) {
137597
138164
  const message = err instanceof Error ? err.message : String(err);
137598
138165
  console.error(`
@@ -137656,14 +138223,15 @@ function registerGoogleAdsDiagnosisCommands(program2) {
137656
138223
  "--no-merge-collect",
137657
138224
  "\u4E0D\u4ECE collect \u81EA\u52A8\u6062\u590D campaigns/geographic/keywords.items",
137658
138225
  false
137659
- ).option("--lenient", "\u7ED3\u6784\u8B66\u544A\uFF08\u5982\u7F3A analysis\uFF09\u65F6\u4E0D\u963B\u65AD\u6E32\u67D3", false).action(
138226
+ ).option("--lenient", "\u7ED3\u6784\u8B66\u544A\uFF08\u5982\u7F3A analysis\uFF09\u65F6\u4E0D\u963B\u65AD\u6E32\u67D3", false).option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(
137660
138227
  async (opts) => {
137661
138228
  await runGoogleAdsDiagnosisRender({
137662
138229
  dataFile: opts.data,
137663
138230
  out: opts.out,
137664
138231
  collectFile: opts.collect,
137665
138232
  noMergeCollect: opts.noMergeCollect,
137666
- lenient: opts.lenient
138233
+ lenient: opts.lenient,
138234
+ lang: opts.lang
137667
138235
  });
137668
138236
  }
137669
138237
  );
@@ -138044,15 +138612,27 @@ function utcToIso(ms) {
138044
138612
  const d = new Date(ms);
138045
138613
  return isoDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
138046
138614
  }
138047
- function reportPeriodChrome(current, previous) {
138048
- const kind = classifyReportPeriod(current, previous);
138615
+ function reportPeriodChrome(current, previous, lang = "zh") {
138616
+ const kind = classifyReportPeriod(current, previous, lang);
138049
138617
  if (kind.unit === "\u5B63\u5EA6") {
138050
- return { ...kind, reportTitle: "Google \u5B63\u5EA6\u7ECF\u8425\u62A5\u544A", coverEn: "QUARTERLY REPORT" };
138618
+ return {
138619
+ ...kind,
138620
+ reportTitle: lang === "en" ? "Google quarterly business report" : "Google \u5B63\u5EA6\u7ECF\u8425\u62A5\u544A",
138621
+ coverEn: "QUARTERLY REPORT"
138622
+ };
138051
138623
  }
138052
138624
  if (kind.unit === "\u6708\u5EA6") {
138053
- return { ...kind, reportTitle: "Google \u6708\u5EA6\u7ECF\u8425\u62A5\u544A", coverEn: "MONTHLY REPORT" };
138625
+ return {
138626
+ ...kind,
138627
+ reportTitle: lang === "en" ? "Google monthly business report" : "Google \u6708\u5EA6\u7ECF\u8425\u62A5\u544A",
138628
+ coverEn: "MONTHLY REPORT"
138629
+ };
138054
138630
  }
138055
- return { ...kind, reportTitle: "Google \u7ECF\u8425\u62A5\u544A", coverEn: "PERFORMANCE REPORT" };
138631
+ return {
138632
+ ...kind,
138633
+ reportTitle: lang === "en" ? "Google business report" : "Google \u7ECF\u8425\u62A5\u544A",
138634
+ coverEn: "PERFORMANCE REPORT"
138635
+ };
138056
138636
  }
138057
138637
  function lastCompleteQuarter(now = /* @__PURE__ */ new Date()) {
138058
138638
  const parts = new Intl.DateTimeFormat("en-CA", {
@@ -138892,6 +139472,8 @@ import { fileURLToPath as fileURLToPath8 } from "url";
138892
139472
  var TSO_CLI_ROOT = path31.join(path31.dirname(fileURLToPath8(import.meta.url)), "../..");
138893
139473
 
138894
139474
  // src/commands/google-quarterly/render-report.ts
139475
+ init_lang();
139476
+ init_html();
138895
139477
  function asRecord18(value) {
138896
139478
  if (value && typeof value === "object" && !Array.isArray(value)) {
138897
139479
  return value;
@@ -138941,8 +139523,16 @@ async function runGoogleQuarterlyRender(opts) {
138941
139523
  }
138942
139524
  const outPath = path32.resolve(opts.out ?? path32.join(path32.dirname(dataPath), GOOGLE_QUARTERLY_HTML_BASENAME));
138943
139525
  await fs27.mkdir(path32.dirname(outPath), { recursive: true });
138944
- const periodChrome = reportPeriodChrome(readMetaRange(merged, "current"), readMetaRange(merged, "previous"));
138945
- const outHtml = injectQuarterlyReport(html, { ...merged, periodChrome });
139526
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: merged.meta });
139527
+ const periodChrome = reportPeriodChrome(
139528
+ readMetaRange(merged, "current"),
139529
+ readMetaRange(merged, "previous"),
139530
+ lang
139531
+ );
139532
+ const outHtml = injectQuarterlyReport(
139533
+ localizeReportHtml(html, lang),
139534
+ stampMetaLang({ ...merged, periodChrome }, lang)
139535
+ );
138946
139536
  if (!outHtml.includes("homeCDN/echarts.min.js") || /new Chart\s*\(/.test(outHtml)) {
138947
139537
  console.error("\n\u274C \u6A21\u677F\u5FC5\u987B\u4F7F\u7528 ECharts\uFF0C\u7981\u6B62 Chart.js\u3002\n");
138948
139538
  process.exit(1);
@@ -138994,14 +139584,15 @@ function registerGoogleQuarterlyCommands(program2) {
138994
139584
  });
138995
139585
  }
138996
139586
  );
138997
- const render = root.command("render").description("\u5C06 Agent narrative + collect \u4E8B\u5B9E\u6CE8\u5165\u5B63\u5EA6 HTML").requiredOption("--data <file>", "Agent \u64B0\u5199\u7684 google-quarterly.json").option("--collect <file>", "collect JSON\uFF08\u9ED8\u8BA4\u540C\u76EE\u5F55 google-quarterly-collect.json\uFF09").option("--out <file>", "\u8F93\u51FA HTML").option("--lenient", "narrative \u7F3A\u9879\u65F6\u4ECD\u51FA HTML", false);
139587
+ const render = root.command("render").description("\u5C06 Agent narrative + collect \u4E8B\u5B9E\u6CE8\u5165\u5B63\u5EA6 HTML").requiredOption("--data <file>", "Agent \u64B0\u5199\u7684 google-quarterly.json").option("--collect <file>", "collect JSON\uFF08\u9ED8\u8BA4\u540C\u76EE\u5F55 google-quarterly-collect.json\uFF09").option("--out <file>", "\u8F93\u51FA HTML").option("--lenient", "narrative \u7F3A\u9879\u65F6\u4ECD\u51FA HTML", false).option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587");
138998
139588
  render.action(
138999
139589
  async (opts) => {
139000
139590
  await runGoogleQuarterlyRender({
139001
139591
  dataFile: opts.data,
139002
139592
  collectFile: opts.collect,
139003
139593
  out: opts.out,
139004
- lenient: opts.lenient
139594
+ lenient: opts.lenient,
139595
+ lang: opts.lang
139005
139596
  });
139006
139597
  }
139007
139598
  );
@@ -139412,6 +140003,9 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
139412
140003
  };
139413
140004
  }
139414
140005
 
140006
+ // src/commands/facebook-analysis/render-excel.ts
140007
+ init_html();
140008
+
139415
140009
  // src/commands/facebook-analysis/write-xlsx.ts
139416
140010
  init_xlsx_style_engine();
139417
140011
  var MIN_COL_WIDTH = 10;
@@ -139620,8 +140214,18 @@ function buildMetaPeriodExcelSheets(data) {
139620
140214
  { name: "\u53D7\u4F17", rows: audienceRows }
139621
140215
  ];
139622
140216
  }
139623
- function writeMetaPeriodExcel(outPath, data) {
139624
- writeSimpleXlsx(outPath, buildMetaPeriodExcelSheets(data));
140217
+ function chrome2(text3, lang) {
140218
+ return lang === "en" ? applyEnglishPhrases(text3) : text3;
140219
+ }
140220
+ function writeMetaPeriodExcel(outPath, data, lang = "zh") {
140221
+ const sheets = buildMetaPeriodExcelSheets(data).map((sheet) => ({
140222
+ ...sheet,
140223
+ name: chrome2(sheet.name, lang),
140224
+ rows: sheet.rows.map(
140225
+ (row, index) => index === 0 ? row.map((cell5) => typeof cell5 === "string" ? chrome2(cell5, lang) : cell5) : row
140226
+ )
140227
+ }));
140228
+ writeSimpleXlsx(outPath, sheets);
139625
140229
  }
139626
140230
 
139627
140231
  // src/commands/facebook-analysis/report-content.ts
@@ -139911,6 +140515,8 @@ function formatMetaPeriodReportErrors(result) {
139911
140515
 
139912
140516
  // src/commands/facebook-analysis/render-report.ts
139913
140517
  init_report_period();
140518
+ init_lang();
140519
+ init_html();
139914
140520
  var TEMPLATE_BASENAMES2 = {
139915
140521
  html: "meta-period-report.html"
139916
140522
  };
@@ -139959,12 +140565,13 @@ function asRecord27(value) {
139959
140565
  }
139960
140566
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
139961
140567
  }
139962
- function injectReportData3(html, payload) {
139963
- const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
139964
- if (!html.includes(INJECT_MARKER2)) {
140568
+ function injectReportData3(html, payload, lang) {
140569
+ const localized = localizeReportHtml(html, lang);
140570
+ const json = JSON.stringify(attachPeriodChrome(stampMetaLang(payload, lang), lang)).replace(/</g, "\\u003c");
140571
+ if (!localized.includes(INJECT_MARKER2)) {
139965
140572
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 meta-period-report.html");
139966
140573
  }
139967
- return html.replace(INJECT_MARKER2, `window.__META_PERIOD_REPORT__ = ${json};`);
140574
+ return localized.replace(INJECT_MARKER2, `window.__META_PERIOD_REPORT__ = ${json};`);
139968
140575
  }
139969
140576
  function defaultOutPath(dataPath, format, out) {
139970
140577
  if (out?.trim()) {
@@ -139987,6 +140594,7 @@ async function runFacebookAnalysisRender(opts) {
139987
140594
  data = await mergeFacebookSnapshotIntoReport(data, path42.resolve(opts.snapshotDir));
139988
140595
  }
139989
140596
  localizeFacebookReportLabels(data);
140597
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
139990
140598
  const contentCheck = validateMetaPeriodReportContent(data);
139991
140599
  if (!contentCheck.ok) {
139992
140600
  console.error(`
@@ -140005,7 +140613,7 @@ async function runFacebookAnalysisRender(opts) {
140005
140613
  const outPath = defaultOutPath(dataPath, format, opts.out);
140006
140614
  await fsPromises3.mkdir(path42.dirname(outPath), { recursive: true });
140007
140615
  if (format === "xlsx") {
140008
- writeMetaPeriodExcel(outPath, data);
140616
+ writeMetaPeriodExcel(outPath, data, lang);
140009
140617
  } else {
140010
140618
  const templatePath = metaPeriodReportTemplatePath();
140011
140619
  let html;
@@ -140018,7 +140626,7 @@ async function runFacebookAnalysisRender(opts) {
140018
140626
  `);
140019
140627
  process.exit(1);
140020
140628
  }
140021
- await fsPromises3.writeFile(outPath, injectReportData3(html, data), "utf8");
140629
+ await fsPromises3.writeFile(outPath, injectReportData3(html, data, lang), "utf8");
140022
140630
  }
140023
140631
  written.push(outPath);
140024
140632
  }
@@ -140389,6 +140997,8 @@ function formatMetaDiagnosisReportErrors(result) {
140389
140997
  }
140390
140998
 
140391
140999
  // src/commands/facebook-analysis/diagnosis-render-report.ts
141000
+ init_lang();
141001
+ init_html();
140392
141002
  var TEMPLATE_BASENAME2 = "meta-account-diagnosis-report.html";
140393
141003
  var INJECT_MARKER3 = "window.__META_DIAGNOSIS_REPORT__ = window.__META_DIAGNOSIS_REPORT__ || null;";
140394
141004
  function resolveSkillTemplatePath5(basename13) {
@@ -140420,12 +141030,13 @@ function asRecord30(value) {
140420
141030
  }
140421
141031
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
140422
141032
  }
140423
- function injectReportData4(html, payload) {
140424
- const json = JSON.stringify(payload).replace(/</g, "\\u003c");
140425
- if (!html.includes(INJECT_MARKER3)) {
141033
+ function injectReportData4(html, payload, lang) {
141034
+ const localized = localizeReportHtml(html, lang);
141035
+ const json = JSON.stringify(stampMetaLang(payload, lang)).replace(/</g, "\\u003c");
141036
+ if (!localized.includes(INJECT_MARKER3)) {
140426
141037
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 meta-account-diagnosis-report.html");
140427
141038
  }
140428
- return html.replace(INJECT_MARKER3, `window.__META_DIAGNOSIS_REPORT__ = ${json};`);
141039
+ return localized.replace(INJECT_MARKER3, `window.__META_DIAGNOSIS_REPORT__ = ${json};`);
140429
141040
  }
140430
141041
  async function runFacebookAnalysisDiagnosisRender(opts) {
140431
141042
  const dataPath = path44.resolve(opts.dataFile);
@@ -140433,6 +141044,7 @@ async function runFacebookAnalysisDiagnosisRender(opts) {
140433
141044
  if (opts.snapshotDir?.trim()) {
140434
141045
  data = await mergeMetaDiagnosisSnapshotIntoReport(data, path44.resolve(opts.snapshotDir));
140435
141046
  }
141047
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
140436
141048
  const contentCheck = validateMetaDiagnosisReportContent(data);
140437
141049
  if (!contentCheck.ok) {
140438
141050
  console.error(`
@@ -140462,7 +141074,7 @@ async function runFacebookAnalysisDiagnosisRender(opts) {
140462
141074
  );
140463
141075
  const outDir = path44.dirname(outPath);
140464
141076
  await fsPromises4.mkdir(outDir, { recursive: true });
140465
- await fsPromises4.writeFile(outPath, injectReportData4(html, data), "utf8");
141077
+ await fsPromises4.writeFile(outPath, injectReportData4(html, data, lang), "utf8");
140466
141078
  const { stats } = contentCheck;
140467
141079
  console.log(`
140468
141080
  \u2705 Meta\uFF08Facebook\uFF09\u8D26\u6237\u8BCA\u65AD HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
@@ -141152,12 +141764,13 @@ function registerFacebookAnalysisCommands(program2) {
141152
141764
  "--format <html|xlsx>",
141153
141765
  "\u9ED8\u8BA4 html\uFF08\u8FD0\u8425\u6D45\u8272\u6A21\u677F\uFF09\uFF1B\u7528\u6237\u8981\u8868\u683C / Excel \u65F6\u4F20 xlsx\uFF1B\u53EF\u9017\u53F7\u540C\u65F6\u8F93\u51FA html,xlsx",
141154
141766
  "html"
141155
- ).action(async (opts) => {
141767
+ ).option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(async (opts) => {
141156
141768
  await runFacebookAnalysisRender({
141157
141769
  dataFile: opts.data,
141158
141770
  snapshotDir: opts.snapshotDir,
141159
141771
  out: opts.out,
141160
- format: opts.format
141772
+ format: opts.format,
141773
+ lang: opts.lang
141161
141774
  });
141162
141775
  });
141163
141776
  root.command("diagnosis-render").description(
@@ -141165,11 +141778,12 @@ function registerFacebookAnalysisCommands(program2) {
141165
141778
  ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 meta-account-diagnosis-report.json").option(
141166
141779
  "--snapshot-dir <dir>",
141167
141780
  "\u53EF\u9009\uFF1Afacebook-analysis --json-out \u76EE\u5F55\uFF0C\u81EA\u52A8\u8865\u5168 meta / kpis\uFF08overview \u5FEB\u7167\uFF09"
141168
- ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
141781
+ ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(async (opts) => {
141169
141782
  await runFacebookAnalysisDiagnosisRender({
141170
141783
  dataFile: opts.data,
141171
141784
  snapshotDir: opts.snapshotDir,
141172
- out: opts.out
141785
+ out: opts.out,
141786
+ lang: opts.lang
141173
141787
  });
141174
141788
  });
141175
141789
  root.command("account-status").description(
@@ -142521,6 +143135,8 @@ function formatBingPeriodReportErrors(result) {
142521
143135
 
142522
143136
  // src/commands/bing-analysis/render-report.ts
142523
143137
  init_report_period();
143138
+ init_lang();
143139
+ init_html();
142524
143140
  var TEMPLATE_BASENAME3 = "bing-period-report.html";
142525
143141
  var INJECT_MARKER4 = "window.__BING_PERIOD_REPORT__ = window.__BING_PERIOD_REPORT__ || null;";
142526
143142
  function resolveSkillTemplatePath6(basename13) {
@@ -142552,12 +143168,13 @@ function asRecord34(value) {
142552
143168
  }
142553
143169
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
142554
143170
  }
142555
- function injectReportData5(html, payload) {
142556
- const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
142557
- if (!html.includes(INJECT_MARKER4)) {
143171
+ function injectReportData5(html, payload, lang) {
143172
+ const localized = localizeReportHtml(html, lang);
143173
+ const json = JSON.stringify(attachPeriodChrome(stampMetaLang(payload, lang), lang)).replace(/</g, "\\u003c");
143174
+ if (!localized.includes(INJECT_MARKER4)) {
142558
143175
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 bing-period-report.html");
142559
143176
  }
142560
- return html.replace(INJECT_MARKER4, `window.__BING_PERIOD_REPORT__ = ${json};`);
143177
+ return localized.replace(INJECT_MARKER4, `window.__BING_PERIOD_REPORT__ = ${json};`);
142561
143178
  }
142562
143179
  async function runBingAnalysisRender(opts) {
142563
143180
  const dataPath = path48.resolve(opts.dataFile);
@@ -142565,6 +143182,7 @@ async function runBingAnalysisRender(opts) {
142565
143182
  if (opts.snapshotDir?.trim()) {
142566
143183
  data = await mergeBingAnalysisSnapshotIntoReport(data, path48.resolve(opts.snapshotDir));
142567
143184
  }
143185
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
142568
143186
  const contentCheck = validateBingPeriodReportContent(data);
142569
143187
  if (!contentCheck.ok) {
142570
143188
  console.error(`
@@ -142594,7 +143212,7 @@ async function runBingAnalysisRender(opts) {
142594
143212
  );
142595
143213
  const outDir = path48.dirname(outPath);
142596
143214
  await fsPromises5.mkdir(outDir, { recursive: true });
142597
- await fsPromises5.writeFile(outPath, injectReportData5(html, data), "utf8");
143215
+ await fsPromises5.writeFile(outPath, injectReportData5(html, data, lang), "utf8");
142598
143216
  const { stats } = contentCheck;
142599
143217
  const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
142600
143218
  console.log(`
@@ -142661,11 +143279,12 @@ function registerBingAnalysisCommands(program2) {
142661
143279
  ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 bing-period-report.json").option(
142662
143280
  "--snapshot-dir <dir>",
142663
143281
  "\u53EF\u9009\uFF1Abing-analysis --json-out \u76EE\u5F55\uFF0C\u81EA\u52A8\u8865\u5168 meta / kpis\uFF08overview \u5FEB\u7167\uFF09"
142664
- ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
143282
+ ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(async (opts) => {
142665
143283
  await runBingAnalysisRender({
142666
143284
  dataFile: opts.data,
142667
143285
  snapshotDir: opts.snapshotDir,
142668
- out: opts.out
143286
+ out: opts.out,
143287
+ lang: opts.lang
142669
143288
  });
142670
143289
  });
142671
143290
  root.command("account-status").description("\u8D26\u6237\u72B6\u6001\uFF08GET \u2026/account-status\uFF1Bstatus=Active \u4E3A\u6B63\u5E38\u6295\u653E\uFF1B\u4E0D\u5E26\u65E5\u671F\u533A\u95F4\uFF09").requiredOption(...ACCOUNT_OPTION2).option("-t, --token <token>", "Auth Token").option("--json-out <path>", JSON_OUT_OPTION_DESC2, void 0).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
@@ -143420,6 +144039,8 @@ function formatTikTokPeriodReportErrors(result) {
143420
144039
 
143421
144040
  // src/commands/tiktok-analysis/render-report.ts
143422
144041
  init_report_period();
144042
+ init_lang();
144043
+ init_html();
143423
144044
  var TEMPLATE_BASENAME4 = "tiktok-period-report.html";
143424
144045
  var INJECT_MARKER5 = "window.__TIKTOK_PERIOD_REPORT__ = window.__TIKTOK_PERIOD_REPORT__ || null;";
143425
144046
  function resolveSkillTemplatePath7(basename13) {
@@ -143451,12 +144072,13 @@ function asRecord37(value) {
143451
144072
  }
143452
144073
  throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
143453
144074
  }
143454
- function injectReportData6(html, payload) {
143455
- const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
143456
- if (!html.includes(INJECT_MARKER5)) {
144075
+ function injectReportData6(html, payload, lang) {
144076
+ const localized = localizeReportHtml(html, lang);
144077
+ const json = JSON.stringify(attachPeriodChrome(stampMetaLang(payload, lang), lang)).replace(/</g, "\\u003c");
144078
+ if (!localized.includes(INJECT_MARKER5)) {
143457
144079
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 tiktok-period-report.html");
143458
144080
  }
143459
- return html.replace(INJECT_MARKER5, `window.__TIKTOK_PERIOD_REPORT__ = ${json};`);
144081
+ return localized.replace(INJECT_MARKER5, `window.__TIKTOK_PERIOD_REPORT__ = ${json};`);
143460
144082
  }
143461
144083
  async function runTikTokAnalysisRender(opts) {
143462
144084
  const dataPath = path51.resolve(opts.dataFile);
@@ -143464,6 +144086,7 @@ async function runTikTokAnalysisRender(opts) {
143464
144086
  if (opts.snapshotDir?.trim()) {
143465
144087
  data = await mergeTikTokAnalysisSnapshotIntoReport(data, path51.resolve(opts.snapshotDir));
143466
144088
  }
144089
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
143467
144090
  const contentCheck = validateTikTokPeriodReportContent(data);
143468
144091
  if (!contentCheck.ok) {
143469
144092
  console.error(`
@@ -143493,7 +144116,7 @@ async function runTikTokAnalysisRender(opts) {
143493
144116
  );
143494
144117
  const outDir = path51.dirname(outPath);
143495
144118
  await fsPromises6.mkdir(outDir, { recursive: true });
143496
- await fsPromises6.writeFile(outPath, injectReportData6(html, data), "utf8");
144119
+ await fsPromises6.writeFile(outPath, injectReportData6(html, data, lang), "utf8");
143497
144120
  const { stats } = contentCheck;
143498
144121
  const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
143499
144122
  console.log(`
@@ -144239,11 +144862,12 @@ function registerTikTokAnalysisCommands(program2) {
144239
144862
  ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 tiktok-period-report.json").option(
144240
144863
  "--snapshot-dir <dir>",
144241
144864
  "\u53EF\u9009\uFF1Atiktok-analysis --json-out \u76EE\u5F55\uFF0C\u81EA\u52A8\u8865\u5168 meta\uFF08accountId / \u65E5\u671F\u533A\u95F4 / \u8D27\u5E01\uFF09"
144242
- ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
144865
+ ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(async (opts) => {
144243
144866
  await runTikTokAnalysisRender({
144244
144867
  dataFile: opts.data,
144245
144868
  snapshotDir: opts.snapshotDir,
144246
- out: opts.out
144869
+ out: opts.out,
144870
+ lang: opts.lang
144247
144871
  });
144248
144872
  });
144249
144873
  root.command("areacode").description("TikTok \u5730\u533A\u4EE3\u7801\u679A\u4E3E\uFF08\u4E3B\u5E73\u53F0 query/media-account/tiktok/TikTokAreacode/Read\uFF09").option("-t, --token <token>", "Auth Token").option(
@@ -145196,6 +145820,8 @@ function formatYandexPeriodReportErrors(result) {
145196
145820
 
145197
145821
  // src/commands/yandex-analysis/render-report.ts
145198
145822
  init_report_period();
145823
+ init_lang();
145824
+ init_html();
145199
145825
  var TEMPLATE_BASENAME5 = "yandex-period-report.html";
145200
145826
  var INJECT_MARKER6 = "window.__YANDEX_PERIOD_REPORT__ = window.__YANDEX_PERIOD_REPORT__ || null;";
145201
145827
  function resolveSkillTemplatePath8(basename13) {
@@ -145240,12 +145866,13 @@ function refreshWeeklyFromDaily(data) {
145240
145866
  }
145241
145867
  };
145242
145868
  }
145243
- function injectReportData7(html, payload) {
145244
- const json = JSON.stringify(attachPeriodChrome(payload)).replace(/</g, "\\u003c");
145245
- if (!html.includes(INJECT_MARKER6)) {
145869
+ function injectReportData7(html, payload, lang) {
145870
+ const localized = localizeReportHtml(html, lang);
145871
+ const json = JSON.stringify(attachPeriodChrome(stampMetaLang(payload, lang), lang)).replace(/</g, "\\u003c");
145872
+ if (!localized.includes(INJECT_MARKER6)) {
145246
145873
  throw new Error("\u62A5\u544A\u6A21\u677F\u7F3A\u5C11\u6570\u636E\u6CE8\u5165\u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 yandex-period-report.html");
145247
145874
  }
145248
- return html.replace(INJECT_MARKER6, `window.__YANDEX_PERIOD_REPORT__ = ${json};`);
145875
+ return localized.replace(INJECT_MARKER6, `window.__YANDEX_PERIOD_REPORT__ = ${json};`);
145249
145876
  }
145250
145877
  async function runYandexAnalysisRender(opts) {
145251
145878
  const dataPath = path54.resolve(opts.dataFile);
@@ -145254,6 +145881,7 @@ async function runYandexAnalysisRender(opts) {
145254
145881
  data = await mergeYandexAnalysisSnapshotIntoReport(data, path54.resolve(opts.snapshotDir));
145255
145882
  }
145256
145883
  data = refreshWeeklyFromDaily(data);
145884
+ const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
145257
145885
  const contentCheck = validateYandexPeriodReportContent(data);
145258
145886
  if (!contentCheck.ok) {
145259
145887
  console.error(`
@@ -145281,7 +145909,7 @@ async function runYandexAnalysisRender(opts) {
145281
145909
  );
145282
145910
  const outDir = path54.dirname(outPath);
145283
145911
  await fsPromises7.mkdir(outDir, { recursive: true });
145284
- await fsPromises7.writeFile(outPath, injectReportData7(html, data), "utf8");
145912
+ await fsPromises7.writeFile(outPath, injectReportData7(html, data, lang), "utf8");
145285
145913
  const { stats } = contentCheck;
145286
145914
  const cover = periodWord(attachPeriodChrome(data).periodChrome.unit, "\u8D26\u6237");
145287
145915
  console.log(`
@@ -145793,11 +146421,12 @@ function registerYandexAnalysisCommands(program2) {
145793
146421
  ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 yandex-period-report.json").option(
145794
146422
  "--snapshot-dir <dir>",
145795
146423
  "\u53EF\u9009\uFF1Ayandex-analysis --json-out \u76EE\u5F55\uFF0C\u81EA\u52A8\u8865\u5168 meta / kpis / tables"
145796
- ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
146424
+ ).option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option("--lang <lang>", "\u7EC8\u7A3F\u8BED\u8A00\uFF1Azh \u6216 en\u3002\u8986\u76D6 JSON meta.lang\uFF1B\u90FD\u6CA1\u6709\u5219\u4E2D\u6587").action(async (opts) => {
145797
146425
  await runYandexAnalysisRender({
145798
146426
  dataFile: opts.data,
145799
146427
  snapshotDir: opts.snapshotDir,
145800
- out: opts.out
146428
+ out: opts.out,
146429
+ lang: opts.lang
145801
146430
  });
145802
146431
  });
145803
146432
  root.command("account-status").description(