pabal-resource-mcp 1.5.5 → 1.5.7

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 (2) hide show
  1. package/dist/bin/mcp-server.js +174 -155
  2. package/package.json +1 -1
@@ -3448,127 +3448,26 @@ var DEVICE_ASPECT_RATIOS = {
3448
3448
  phone: "9:16",
3449
3449
  tablet: "3:4"
3450
3450
  };
3451
- var GEMINI_SUPPORTED_LOCALES = {
3452
- // English variants
3453
- "en": "EN",
3454
- "en-US": "EN",
3455
- "en-GB": "EN",
3456
- "en-AU": "EN",
3457
- "en-CA": "EN",
3458
- // Arabic
3459
- "ar": "ar-EG",
3460
- "ar-EG": "ar-EG",
3461
- "ar-SA": "ar-EG",
3462
- // German
3463
- "de": "de-DE",
3464
- "de-DE": "de-DE",
3465
- // Spanish
3466
- "es": "es-MX",
3467
- "es-MX": "es-MX",
3468
- "es-ES": "es-MX",
3469
- "es-419": "es-MX",
3470
- // French
3471
- "fr": "fr-FR",
3472
- "fr-FR": "fr-FR",
3473
- "fr-CA": "fr-FR",
3474
- // Hindi
3475
- "hi": "hi-IN",
3476
- "hi-IN": "hi-IN",
3477
- // Indonesian
3478
- "id": "id-ID",
3479
- "id-ID": "id-ID",
3480
- // Italian
3481
- "it": "it-IT",
3482
- "it-IT": "it-IT",
3483
- // Japanese
3484
- "ja": "ja-JP",
3485
- "ja-JP": "ja-JP",
3486
- // Korean
3487
- "ko": "ko-KR",
3488
- "ko-KR": "ko-KR",
3489
- // Portuguese
3490
- "pt": "pt-BR",
3491
- "pt-BR": "pt-BR",
3492
- "pt-PT": "pt-BR",
3493
- // Russian
3494
- "ru": "ru-RU",
3495
- "ru-RU": "ru-RU",
3496
- // Ukrainian
3497
- "uk": "ua-UA",
3498
- "uk-UA": "ua-UA",
3499
- "ua-UA": "ua-UA",
3500
- // Vietnamese
3501
- "vi": "vi-VN",
3502
- "vi-VN": "vi-VN",
3503
- // Chinese
3504
- "zh": "zh-CN",
3505
- "zh-CN": "zh-CN",
3506
- "zh-Hans": "zh-CN",
3507
- "zh-TW": "zh-CN",
3508
- "zh-Hant": "zh-CN"
3509
- };
3510
- function isGeminiSupportedLocale(locale) {
3511
- return locale in GEMINI_SUPPORTED_LOCALES;
3512
- }
3513
- function getUnsupportedLocales(locales) {
3514
- return locales.filter((locale) => !isGeminiSupportedLocale(locale));
3515
- }
3516
- var LANGUAGE_NAMES = {
3517
- "en-US": "English (US)",
3518
- "en-GB": "English (UK)",
3519
- "en-AU": "English (Australia)",
3520
- "en-CA": "English (Canada)",
3521
- "ko-KR": "Korean",
3522
- "ja-JP": "Japanese",
3523
- "zh-Hans": "Simplified Chinese",
3524
- "zh-Hant": "Traditional Chinese",
3525
- "zh-CN": "Simplified Chinese",
3526
- "zh-TW": "Traditional Chinese",
3527
- "fr-FR": "French",
3528
- "fr-CA": "French (Canada)",
3451
+ var GEMINI_LANGUAGE_NAMES = {
3452
+ "en-US": "English",
3453
+ "ar-EG": "Arabic",
3529
3454
  "de-DE": "German",
3530
- "es-ES": "Spanish (Spain)",
3531
- "es-419": "Spanish (Latin America)",
3532
- "es-MX": "Spanish (Mexico)",
3533
- "pt-BR": "Portuguese (Brazil)",
3534
- "pt-PT": "Portuguese (Portugal)",
3455
+ "es-MX": "Spanish",
3456
+ "fr-FR": "French",
3457
+ "hi-IN": "Hindi",
3458
+ "id-ID": "Indonesian",
3535
3459
  "it-IT": "Italian",
3536
- "nl-NL": "Dutch",
3460
+ "ja-JP": "Japanese",
3461
+ "ko-KR": "Korean",
3462
+ "pt-BR": "Portuguese",
3537
3463
  "ru-RU": "Russian",
3538
- "ar": "Arabic",
3539
- "ar-SA": "Arabic",
3540
- "hi-IN": "Hindi",
3541
- "th-TH": "Thai",
3464
+ "ua-UA": "Ukrainian",
3542
3465
  "vi-VN": "Vietnamese",
3543
- "id-ID": "Indonesian",
3544
- "ms-MY": "Malay",
3545
- "tr-TR": "Turkish",
3546
- "pl-PL": "Polish",
3547
- "uk-UA": "Ukrainian",
3548
- "cs-CZ": "Czech",
3549
- "el-GR": "Greek",
3550
- "ro-RO": "Romanian",
3551
- "hu-HU": "Hungarian",
3552
- "sv-SE": "Swedish",
3553
- "da-DK": "Danish",
3554
- "fi-FI": "Finnish",
3555
- "no-NO": "Norwegian",
3556
- "he-IL": "Hebrew",
3557
- "sk-SK": "Slovak",
3558
- "bg-BG": "Bulgarian",
3559
- "hr-HR": "Croatian",
3560
- "ca-ES": "Catalan"
3466
+ "zh-CN": "Chinese"
3561
3467
  };
3562
3468
  function getLanguageName(locale) {
3563
- if (LANGUAGE_NAMES[locale]) {
3564
- return LANGUAGE_NAMES[locale];
3565
- }
3566
- const baseCode = locale.split("-")[0];
3567
- const matchingKey = Object.keys(LANGUAGE_NAMES).find(
3568
- (key) => key.startsWith(baseCode + "-") || key === baseCode
3569
- );
3570
- if (matchingKey) {
3571
- return LANGUAGE_NAMES[matchingKey];
3469
+ if (locale in GEMINI_LANGUAGE_NAMES) {
3470
+ return GEMINI_LANGUAGE_NAMES[locale];
3572
3471
  }
3573
3472
  return locale;
3574
3473
  }
@@ -3591,7 +3490,7 @@ function readImageAsBase64(imagePath) {
3591
3490
  function getAspectRatioForDevice(deviceType) {
3592
3491
  return DEVICE_ASPECT_RATIOS[deviceType];
3593
3492
  }
3594
- async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath, deviceType, preserveWords) {
3493
+ async function translateImage(sourcePath, sourceLocale, targetLocale, outputPaths, deviceType, preserveWords) {
3595
3494
  try {
3596
3495
  const client = getGeminiClient();
3597
3496
  const sourceLanguage = getLanguageName(sourceLocale);
@@ -3650,14 +3549,17 @@ IMPORTANT INSTRUCTIONS:
3650
3549
  for (const part of parts) {
3651
3550
  if (part.inlineData?.data) {
3652
3551
  const imageBuffer = Buffer.from(part.inlineData.data, "base64");
3653
- const outputDir = path10.dirname(outputPath);
3654
- if (!fs10.existsSync(outputDir)) {
3655
- fs10.mkdirSync(outputDir, { recursive: true });
3552
+ for (const outputPath of outputPaths) {
3553
+ const outputDir = path10.dirname(outputPath);
3554
+ if (!fs10.existsSync(outputDir)) {
3555
+ fs10.mkdirSync(outputDir, { recursive: true });
3556
+ }
3557
+ await sharp(imageBuffer).png().toFile(outputPath);
3656
3558
  }
3657
- await sharp(imageBuffer).png().toFile(outputPath);
3658
3559
  return {
3659
3560
  success: true,
3660
- outputPath
3561
+ outputPath: outputPaths[0]
3562
+ // Return primary path
3661
3563
  };
3662
3564
  }
3663
3565
  }
@@ -3695,7 +3597,7 @@ async function translateImagesWithProgress(translations, onProgress, preserveWor
3695
3597
  translation.sourcePath,
3696
3598
  translation.sourceLocale,
3697
3599
  translation.targetLocale,
3698
- translation.outputPath,
3600
+ translation.outputPaths,
3699
3601
  translation.deviceType,
3700
3602
  preserveWords
3701
3603
  );
@@ -3717,6 +3619,81 @@ async function translateImagesWithProgress(translations, onProgress, preserveWor
3717
3619
  return { successful, failed, errors };
3718
3620
  }
3719
3621
 
3622
+ // src/tools/aso/utils/localize-screenshots/locale-mapping.constants.ts
3623
+ var GEMINI_LOCALE_GROUPS = {
3624
+ // English - covers all English variants
3625
+ "en-US": ["en-US", "en-AU", "en-CA", "en-GB", "en-IN", "en-SG", "en-ZA"],
3626
+ // Arabic - Gemini ar-EG → Unified ar
3627
+ "ar-EG": ["ar"],
3628
+ // German
3629
+ "de-DE": ["de-DE"],
3630
+ // Spanish - covers Latin America, Spain, and US Spanish
3631
+ "es-MX": ["es-419", "es-ES", "es-US"],
3632
+ // French - covers France and Canada
3633
+ "fr-FR": ["fr-FR", "fr-CA"],
3634
+ // Hindi
3635
+ "hi-IN": ["hi-IN"],
3636
+ // Indonesian
3637
+ "id-ID": ["id-ID"],
3638
+ // Italian
3639
+ "it-IT": ["it-IT"],
3640
+ // Japanese
3641
+ "ja-JP": ["ja-JP"],
3642
+ // Korean
3643
+ "ko-KR": ["ko-KR"],
3644
+ // Portuguese - covers Brazil and Portugal
3645
+ "pt-BR": ["pt-BR", "pt-PT"],
3646
+ // Russian
3647
+ "ru-RU": ["ru-RU"],
3648
+ // Ukrainian - Gemini ua-UA → Unified uk-UA
3649
+ "ua-UA": ["uk-UA"],
3650
+ // Vietnamese
3651
+ "vi-VN": ["vi-VN"],
3652
+ // Chinese - covers Simplified, Traditional, and Hong Kong
3653
+ "zh-CN": ["zh-Hans", "zh-Hant", "zh-HK"]
3654
+ };
3655
+ function getGeminiLocale(unifiedLocale) {
3656
+ for (const [geminiLocale, unifiedLocales] of Object.entries(
3657
+ GEMINI_LOCALE_GROUPS
3658
+ )) {
3659
+ if (unifiedLocales.includes(unifiedLocale)) {
3660
+ return geminiLocale;
3661
+ }
3662
+ }
3663
+ return null;
3664
+ }
3665
+ function prepareLocalesForTranslation(locales, primaryLocale) {
3666
+ const targetLocales = locales.filter((l) => l !== primaryLocale);
3667
+ const localeMapping = /* @__PURE__ */ new Map();
3668
+ const geminiLocalesNeeded = /* @__PURE__ */ new Set();
3669
+ const skippedLocales = [];
3670
+ for (const locale of targetLocales) {
3671
+ const geminiLocale = getGeminiLocale(locale);
3672
+ if (geminiLocale === null) {
3673
+ skippedLocales.push(locale);
3674
+ continue;
3675
+ }
3676
+ geminiLocalesNeeded.add(geminiLocale);
3677
+ const existing = localeMapping.get(geminiLocale) || [];
3678
+ if (!existing.includes(locale)) {
3679
+ existing.push(locale);
3680
+ }
3681
+ localeMapping.set(geminiLocale, existing);
3682
+ }
3683
+ const groupedLocales = [];
3684
+ for (const [, unifiedLocales] of localeMapping) {
3685
+ if (unifiedLocales.length > 1) {
3686
+ groupedLocales.push(...unifiedLocales.slice(1));
3687
+ }
3688
+ }
3689
+ return {
3690
+ translatableLocales: Array.from(geminiLocalesNeeded),
3691
+ localeMapping,
3692
+ skippedLocales,
3693
+ groupedLocales
3694
+ };
3695
+ }
3696
+
3720
3697
  // src/tools/aso/utils/localize-screenshots/image-resizer.util.ts
3721
3698
  import sharp2 from "sharp";
3722
3699
  import fs11 from "fs";
@@ -3846,7 +3823,9 @@ var localizeScreenshotsInputSchema = z7.object({
3846
3823
  "Specific target locales to translate to. If not provided, all supported locales from the product will be used."
3847
3824
  ),
3848
3825
  deviceTypes: z7.array(z7.enum(["phone", "tablet"])).optional().default(["phone", "tablet"]).describe("Device types to process (default: both phone and tablet)"),
3849
- dryRun: z7.boolean().optional().default(false).describe("Preview mode - shows what would be translated without actually translating"),
3826
+ dryRun: z7.boolean().optional().default(false).describe(
3827
+ "Preview mode - shows what would be translated without actually translating"
3828
+ ),
3850
3829
  skipExisting: z7.boolean().optional().default(true).describe("Skip translation if target file already exists (default: true)"),
3851
3830
  screenshotNumbers: z7.union([
3852
3831
  z7.array(z7.number().int().positive()),
@@ -3927,7 +3906,7 @@ function getSupportedLocales(slug) {
3927
3906
  };
3928
3907
  }
3929
3908
  function getTargetLocales(allLocales, primaryLocale, requestedTargets) {
3930
- let targets = allLocales.filter((locale) => locale !== primaryLocale);
3909
+ let localesToProcess = allLocales;
3931
3910
  if (requestedTargets && requestedTargets.length > 0) {
3932
3911
  const validTargets = requestedTargets.filter((t) => allLocales.includes(t));
3933
3912
  const invalidTargets = requestedTargets.filter(
@@ -3938,31 +3917,43 @@ function getTargetLocales(allLocales, primaryLocale, requestedTargets) {
3938
3917
  `Warning: Some requested locales are not in product: ${invalidTargets.join(", ")}`
3939
3918
  );
3940
3919
  }
3941
- targets = validTargets.filter((t) => t !== primaryLocale);
3920
+ localesToProcess = validTargets;
3942
3921
  }
3943
- const skippedLocales = getUnsupportedLocales(targets);
3944
- const supportedTargets = targets.filter((t) => isGeminiSupportedLocale(t));
3945
- return { targets: supportedTargets, skippedLocales };
3922
+ localesToProcess = localesToProcess.filter((l) => l !== primaryLocale);
3923
+ const { translatableLocales, localeMapping, skippedLocales, groupedLocales } = prepareLocalesForTranslation(localesToProcess, primaryLocale);
3924
+ return {
3925
+ targets: translatableLocales,
3926
+ skippedLocales,
3927
+ groupedLocales,
3928
+ localeMapping
3929
+ };
3946
3930
  }
3947
- function buildTranslationTasks(slug, screenshots, primaryLocale, targetLocales, skipExisting) {
3931
+ function buildTranslationTasks(slug, screenshots, primaryLocale, targetLocales, localeMapping, skipExisting) {
3948
3932
  const tasks = [];
3949
3933
  const screenshotsDir = getScreenshotsDir(slug);
3950
3934
  for (const targetLocale of targetLocales) {
3935
+ const outputLocales = localeMapping.get(targetLocale) || [];
3951
3936
  for (const screenshot of screenshots) {
3952
- const outputPath = path11.join(
3953
- screenshotsDir,
3954
- targetLocale,
3955
- screenshot.type,
3956
- screenshot.filename
3957
- );
3958
- if (skipExisting && fs12.existsSync(outputPath)) {
3937
+ const outputPaths = [];
3938
+ for (const locale of outputLocales) {
3939
+ const outputPath = path11.join(
3940
+ screenshotsDir,
3941
+ locale,
3942
+ screenshot.type,
3943
+ screenshot.filename
3944
+ );
3945
+ if (!skipExisting || !fs12.existsSync(outputPath)) {
3946
+ outputPaths.push(outputPath);
3947
+ }
3948
+ }
3949
+ if (outputPaths.length === 0) {
3959
3950
  continue;
3960
3951
  }
3961
3952
  tasks.push({
3962
3953
  sourcePath: screenshot.fullPath,
3963
3954
  sourceLocale: primaryLocale,
3964
3955
  targetLocale,
3965
- outputPath,
3956
+ outputPaths,
3966
3957
  deviceType: screenshot.type,
3967
3958
  filename: screenshot.filename
3968
3959
  });
@@ -4013,11 +4004,12 @@ async function handleLocalizeScreenshots(input) {
4013
4004
  ]
4014
4005
  };
4015
4006
  }
4016
- const { targets: targetLocales, skippedLocales } = getTargetLocales(
4017
- allLocales,
4018
- primaryLocale,
4019
- requestedTargetLocales
4020
- );
4007
+ const {
4008
+ targets: targetLocales,
4009
+ skippedLocales,
4010
+ groupedLocales,
4011
+ localeMapping
4012
+ } = getTargetLocales(allLocales, primaryLocale, requestedTargetLocales);
4021
4013
  if (targetLocales.length === 0) {
4022
4014
  const skippedMsg = skippedLocales.length > 0 ? ` (Skipped due to Gemini limitation: ${skippedLocales.join(", ")})` : "";
4023
4015
  return {
@@ -4029,9 +4021,16 @@ async function handleLocalizeScreenshots(input) {
4029
4021
  ]
4030
4022
  };
4031
4023
  }
4032
- results.push(`\u{1F3AF} Target locales: ${targetLocales.join(", ")}`);
4024
+ results.push(`\u{1F3AF} Target locales to translate: ${targetLocales.join(", ")}`);
4025
+ if (groupedLocales.length > 0) {
4026
+ results.push(
4027
+ `\u{1F4CB} Grouped locales (saved together): ${groupedLocales.join(", ")}`
4028
+ );
4029
+ }
4033
4030
  if (skippedLocales.length > 0) {
4034
- results.push(`\u26A0\uFE0F Skipped locales (not supported by Gemini): ${skippedLocales.join(", ")}`);
4031
+ results.push(
4032
+ `\u26A0\uFE0F Skipped locales (not supported by Gemini): ${skippedLocales.join(", ")}`
4033
+ );
4035
4034
  }
4036
4035
  const sourceScreenshots = scanLocaleScreenshots(appInfo.slug, primaryLocale);
4037
4036
  let filteredScreenshots = sourceScreenshots.filter(
@@ -4081,14 +4080,21 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4081
4080
  ]
4082
4081
  };
4083
4082
  }
4084
- const phoneCount = filteredScreenshots.filter((s) => s.type === "phone").length;
4085
- const tabletCount = filteredScreenshots.filter((s) => s.type === "tablet").length;
4086
- results.push(`\u{1F4F8} Source screenshots: ${phoneCount} phone, ${tabletCount} tablet`);
4083
+ const phoneCount = filteredScreenshots.filter(
4084
+ (s) => s.type === "phone"
4085
+ ).length;
4086
+ const tabletCount = filteredScreenshots.filter(
4087
+ (s) => s.type === "tablet"
4088
+ ).length;
4089
+ results.push(
4090
+ `\u{1F4F8} Source screenshots: ${phoneCount} phone, ${tabletCount} tablet`
4091
+ );
4087
4092
  const tasks = buildTranslationTasks(
4088
4093
  appInfo.slug,
4089
4094
  filteredScreenshots,
4090
4095
  primaryLocale,
4091
4096
  targetLocales,
4097
+ localeMapping,
4092
4098
  skipExisting
4093
4099
  );
4094
4100
  if (tasks.length === 0) {
@@ -4117,8 +4123,11 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4117
4123
  tasksByLocale[task.targetLocale].push(task);
4118
4124
  }
4119
4125
  for (const [locale, localeTasks] of Object.entries(tasksByLocale)) {
4126
+ const grouped = localeMapping.get(locale) || [];
4127
+ const groupedOthers = grouped.filter((l) => l !== locale);
4128
+ const groupInfo = groupedOthers.length > 0 ? ` \u2192 also: ${groupedOthers.join(", ")}` : "";
4120
4129
  results.push(`
4121
- \u{1F4C1} ${locale}:`);
4130
+ \u{1F4C1} ${locale}${groupInfo}:`);
4122
4131
  for (const task of localeTasks) {
4123
4132
  results.push(` - ${task.deviceType}/${task.filename}`);
4124
4133
  }
@@ -4168,20 +4177,30 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4168
4177
  results.push(` - ${path11.basename(err.path)}: ${err.error}`);
4169
4178
  }
4170
4179
  if (translationResult.errors.length > 5) {
4171
- results.push(` ... and ${translationResult.errors.length - 5} more errors`);
4180
+ results.push(
4181
+ ` ... and ${translationResult.errors.length - 5} more errors`
4182
+ );
4172
4183
  }
4173
4184
  }
4174
4185
  if (translationResult.successful > 0) {
4175
4186
  results.push(`
4176
4187
  \u{1F50D} Validating image dimensions...`);
4177
- const successfulTasks = tasks.filter((t) => fs12.existsSync(t.outputPath));
4178
- const resizePairs = successfulTasks.map((t) => ({
4179
- sourcePath: t.sourcePath,
4180
- translatedPath: t.outputPath
4181
- }));
4188
+ const resizePairs = [];
4189
+ for (const task of tasks) {
4190
+ for (const outputPath of task.outputPaths) {
4191
+ if (fs12.existsSync(outputPath)) {
4192
+ resizePairs.push({
4193
+ sourcePath: task.sourcePath,
4194
+ translatedPath: outputPath
4195
+ });
4196
+ }
4197
+ }
4198
+ }
4182
4199
  const resizeResult = await batchValidateAndResize(resizePairs);
4183
4200
  if (resizeResult.resized > 0) {
4184
- results.push(` \u{1F527} Resized ${resizeResult.resized} images to match source dimensions`);
4201
+ results.push(
4202
+ ` \u{1F527} Resized ${resizeResult.resized} images to match source dimensions`
4203
+ );
4185
4204
  } else {
4186
4205
  results.push(` \u2705 All image dimensions match source`);
4187
4206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-resource-mcp",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",