pabal-resource-mcp 1.5.4 → 1.5.6

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 +191 -104
  2. package/package.json +1 -1
@@ -3432,62 +3432,42 @@ import { GoogleGenAI } from "@google/genai";
3432
3432
  import fs10 from "fs";
3433
3433
  import path10 from "path";
3434
3434
  import sharp from "sharp";
3435
- var LANGUAGE_NAMES = {
3436
- "en-US": "English (US)",
3437
- "en-GB": "English (UK)",
3438
- "en-AU": "English (Australia)",
3439
- "en-CA": "English (Canada)",
3440
- "ko-KR": "Korean",
3441
- "ja-JP": "Japanese",
3442
- "zh-Hans": "Simplified Chinese",
3443
- "zh-Hant": "Traditional Chinese",
3444
- "zh-CN": "Simplified Chinese",
3445
- "zh-TW": "Traditional Chinese",
3446
- "fr-FR": "French",
3447
- "fr-CA": "French (Canada)",
3435
+ var GEMINI_ASPECT_RATIOS = {
3436
+ "1:1": { ratio: 1 / 1, width: 2048, height: 2048 },
3437
+ "2:3": { ratio: 2 / 3, width: 1696, height: 2528 },
3438
+ "3:2": { ratio: 3 / 2, width: 2528, height: 1696 },
3439
+ "3:4": { ratio: 3 / 4, width: 1792, height: 2400 },
3440
+ "4:3": { ratio: 4 / 3, width: 2400, height: 1792 },
3441
+ "4:5": { ratio: 4 / 5, width: 1856, height: 2304 },
3442
+ "5:4": { ratio: 5 / 4, width: 2304, height: 1856 },
3443
+ "9:16": { ratio: 9 / 16, width: 1536, height: 2752 },
3444
+ "16:9": { ratio: 16 / 9, width: 2752, height: 1536 },
3445
+ "21:9": { ratio: 21 / 9, width: 1584, height: 672 }
3446
+ };
3447
+ var DEVICE_ASPECT_RATIOS = {
3448
+ phone: "9:16",
3449
+ tablet: "3:4"
3450
+ };
3451
+ var GEMINI_LANGUAGE_NAMES = {
3452
+ "en-US": "English",
3453
+ "ar-EG": "Arabic",
3448
3454
  "de-DE": "German",
3449
- "es-ES": "Spanish (Spain)",
3450
- "es-419": "Spanish (Latin America)",
3451
- "es-MX": "Spanish (Mexico)",
3452
- "pt-BR": "Portuguese (Brazil)",
3453
- "pt-PT": "Portuguese (Portugal)",
3455
+ "es-MX": "Spanish",
3456
+ "fr-FR": "French",
3457
+ "hi-IN": "Hindi",
3458
+ "id-ID": "Indonesian",
3454
3459
  "it-IT": "Italian",
3455
- "nl-NL": "Dutch",
3460
+ "ja-JP": "Japanese",
3461
+ "ko-KR": "Korean",
3462
+ "pt-BR": "Portuguese",
3456
3463
  "ru-RU": "Russian",
3457
- "ar": "Arabic",
3458
- "ar-SA": "Arabic",
3459
- "hi-IN": "Hindi",
3460
- "th-TH": "Thai",
3464
+ "ua-UA": "Ukrainian",
3461
3465
  "vi-VN": "Vietnamese",
3462
- "id-ID": "Indonesian",
3463
- "ms-MY": "Malay",
3464
- "tr-TR": "Turkish",
3465
- "pl-PL": "Polish",
3466
- "uk-UA": "Ukrainian",
3467
- "cs-CZ": "Czech",
3468
- "el-GR": "Greek",
3469
- "ro-RO": "Romanian",
3470
- "hu-HU": "Hungarian",
3471
- "sv-SE": "Swedish",
3472
- "da-DK": "Danish",
3473
- "fi-FI": "Finnish",
3474
- "no-NO": "Norwegian",
3475
- "he-IL": "Hebrew",
3476
- "sk-SK": "Slovak",
3477
- "bg-BG": "Bulgarian",
3478
- "hr-HR": "Croatian",
3479
- "ca-ES": "Catalan"
3466
+ "zh-CN": "Chinese"
3480
3467
  };
3481
3468
  function getLanguageName(locale) {
3482
- if (LANGUAGE_NAMES[locale]) {
3483
- return LANGUAGE_NAMES[locale];
3484
- }
3485
- const baseCode = locale.split("-")[0];
3486
- const matchingKey = Object.keys(LANGUAGE_NAMES).find(
3487
- (key) => key.startsWith(baseCode + "-") || key === baseCode
3488
- );
3489
- if (matchingKey) {
3490
- return LANGUAGE_NAMES[matchingKey];
3469
+ if (locale in GEMINI_LANGUAGE_NAMES) {
3470
+ return GEMINI_LANGUAGE_NAMES[locale];
3491
3471
  }
3492
3472
  return locale;
3493
3473
  }
@@ -3507,29 +3487,15 @@ function readImageAsBase64(imagePath) {
3507
3487
  }
3508
3488
  return { data: base64, mimeType };
3509
3489
  }
3510
- async function getImageDimensions(imagePath) {
3511
- const metadata = await sharp(imagePath).metadata();
3512
- return {
3513
- width: metadata.width || 1080,
3514
- height: metadata.height || 1920
3515
- };
3490
+ function getAspectRatioForDevice(deviceType) {
3491
+ return DEVICE_ASPECT_RATIOS[deviceType];
3516
3492
  }
3517
- function calculateAspectRatio(width, height) {
3518
- const ratio = width / height;
3519
- if (Math.abs(ratio - 1) < 0.1) return "1:1";
3520
- if (Math.abs(ratio - 9 / 16) < 0.1) return "9:16";
3521
- if (Math.abs(ratio - 16 / 9) < 0.1) return "16:9";
3522
- if (Math.abs(ratio - 3 / 4) < 0.1) return "3:4";
3523
- if (Math.abs(ratio - 4 / 3) < 0.1) return "4:3";
3524
- return ratio < 1 ? "9:16" : "16:9";
3525
- }
3526
- async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath, preserveWords) {
3493
+ async function translateImage(sourcePath, sourceLocale, targetLocale, outputPaths, deviceType, preserveWords) {
3527
3494
  try {
3528
3495
  const client = getGeminiClient();
3529
3496
  const sourceLanguage = getLanguageName(sourceLocale);
3530
3497
  const targetLanguage = getLanguageName(targetLocale);
3531
- const { width, height } = await getImageDimensions(sourcePath);
3532
- const aspectRatio = calculateAspectRatio(width, height);
3498
+ const aspectRatio = getAspectRatioForDevice(deviceType);
3533
3499
  const { data: imageData, mimeType } = readImageAsBase64(sourcePath);
3534
3500
  const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
3535
3501
  - Do NOT translate these words, keep them exactly as-is: ${preserveWords.join(", ")}` : "";
@@ -3583,14 +3549,17 @@ IMPORTANT INSTRUCTIONS:
3583
3549
  for (const part of parts) {
3584
3550
  if (part.inlineData?.data) {
3585
3551
  const imageBuffer = Buffer.from(part.inlineData.data, "base64");
3586
- const outputDir = path10.dirname(outputPath);
3587
- if (!fs10.existsSync(outputDir)) {
3588
- 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);
3589
3558
  }
3590
- await sharp(imageBuffer).png().toFile(outputPath);
3591
3559
  return {
3592
3560
  success: true,
3593
- outputPath
3561
+ outputPath: outputPaths[0]
3562
+ // Return primary path
3594
3563
  };
3595
3564
  }
3596
3565
  }
@@ -3628,7 +3597,8 @@ async function translateImagesWithProgress(translations, onProgress, preserveWor
3628
3597
  translation.sourcePath,
3629
3598
  translation.sourceLocale,
3630
3599
  translation.targetLocale,
3631
- translation.outputPath,
3600
+ translation.outputPaths,
3601
+ translation.deviceType,
3632
3602
  preserveWords
3633
3603
  );
3634
3604
  if (result.success) {
@@ -3649,10 +3619,85 @@ async function translateImagesWithProgress(translations, onProgress, preserveWor
3649
3619
  return { successful, failed, errors };
3650
3620
  }
3651
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
+
3652
3697
  // src/tools/aso/utils/localize-screenshots/image-resizer.util.ts
3653
3698
  import sharp2 from "sharp";
3654
3699
  import fs11 from "fs";
3655
- async function getImageDimensions2(imagePath) {
3700
+ async function getImageDimensions(imagePath) {
3656
3701
  const metadata = await sharp2(imagePath).metadata();
3657
3702
  if (!metadata.width || !metadata.height) {
3658
3703
  throw new Error(`Unable to read dimensions from ${imagePath}`);
@@ -3723,14 +3768,16 @@ async function resizeImage(inputPath, outputPath, targetDimensions) {
3723
3768
  // Preserve aspect ratio
3724
3769
  withoutEnlargement: false,
3725
3770
  // Allow enlargement if needed
3726
- background: bgColor
3771
+ background: bgColor,
3727
3772
  // Use detected edge color
3773
+ kernel: "lanczos3"
3774
+ // High-quality downscaling algorithm
3728
3775
  }).flatten({ background: bgColor }).png().toFile(outputPath + ".tmp");
3729
3776
  fs11.renameSync(outputPath + ".tmp", outputPath);
3730
3777
  }
3731
3778
  async function validateAndResizeImage(sourcePath, translatedPath) {
3732
- const sourceDimensions = await getImageDimensions2(sourcePath);
3733
- const translatedDimensions = await getImageDimensions2(translatedPath);
3779
+ const sourceDimensions = await getImageDimensions(sourcePath);
3780
+ const translatedDimensions = await getImageDimensions(translatedPath);
3734
3781
  const needsResize = sourceDimensions.width !== translatedDimensions.width || sourceDimensions.height !== translatedDimensions.height;
3735
3782
  if (needsResize) {
3736
3783
  await resizeImage(translatedPath, translatedPath, sourceDimensions);
@@ -3857,7 +3904,7 @@ function getSupportedLocales(slug) {
3857
3904
  };
3858
3905
  }
3859
3906
  function getTargetLocales(allLocales, primaryLocale, requestedTargets) {
3860
- let targets = allLocales.filter((locale) => locale !== primaryLocale);
3907
+ let localesToProcess = allLocales;
3861
3908
  if (requestedTargets && requestedTargets.length > 0) {
3862
3909
  const validTargets = requestedTargets.filter((t) => allLocales.includes(t));
3863
3910
  const invalidTargets = requestedTargets.filter(
@@ -3865,32 +3912,50 @@ function getTargetLocales(allLocales, primaryLocale, requestedTargets) {
3865
3912
  );
3866
3913
  if (invalidTargets.length > 0) {
3867
3914
  console.warn(
3868
- `Warning: Some requested locales are not supported: ${invalidTargets.join(", ")}`
3915
+ `Warning: Some requested locales are not in product: ${invalidTargets.join(", ")}`
3869
3916
  );
3870
3917
  }
3871
- targets = validTargets.filter((t) => t !== primaryLocale);
3918
+ localesToProcess = validTargets;
3872
3919
  }
3873
- return targets;
3920
+ const {
3921
+ translatableLocales,
3922
+ localeMapping,
3923
+ skippedLocales,
3924
+ groupedLocales
3925
+ } = prepareLocalesForTranslation(localesToProcess, primaryLocale);
3926
+ return {
3927
+ targets: translatableLocales,
3928
+ skippedLocales,
3929
+ groupedLocales,
3930
+ localeMapping
3931
+ };
3874
3932
  }
3875
- function buildTranslationTasks(slug, screenshots, primaryLocale, targetLocales, skipExisting) {
3933
+ function buildTranslationTasks(slug, screenshots, primaryLocale, targetLocales, localeMapping, skipExisting) {
3876
3934
  const tasks = [];
3877
3935
  const screenshotsDir = getScreenshotsDir(slug);
3878
3936
  for (const targetLocale of targetLocales) {
3937
+ const outputLocales = localeMapping.get(targetLocale) || [];
3879
3938
  for (const screenshot of screenshots) {
3880
- const outputPath = path11.join(
3881
- screenshotsDir,
3882
- targetLocale,
3883
- screenshot.type,
3884
- screenshot.filename
3885
- );
3886
- if (skipExisting && fs12.existsSync(outputPath)) {
3939
+ const outputPaths = [];
3940
+ for (const locale of outputLocales) {
3941
+ const outputPath = path11.join(
3942
+ screenshotsDir,
3943
+ locale,
3944
+ screenshot.type,
3945
+ screenshot.filename
3946
+ );
3947
+ if (!skipExisting || !fs12.existsSync(outputPath)) {
3948
+ outputPaths.push(outputPath);
3949
+ }
3950
+ }
3951
+ if (outputPaths.length === 0) {
3887
3952
  continue;
3888
3953
  }
3889
3954
  tasks.push({
3890
3955
  sourcePath: screenshot.fullPath,
3891
3956
  sourceLocale: primaryLocale,
3892
3957
  targetLocale,
3893
- outputPath,
3958
+ outputPaths,
3894
3959
  deviceType: screenshot.type,
3895
3960
  filename: screenshot.filename
3896
3961
  });
@@ -3941,22 +4006,34 @@ async function handleLocalizeScreenshots(input) {
3941
4006
  ]
3942
4007
  };
3943
4008
  }
3944
- const targetLocales = getTargetLocales(
3945
- allLocales,
3946
- primaryLocale,
3947
- requestedTargetLocales
3948
- );
4009
+ const {
4010
+ targets: targetLocales,
4011
+ skippedLocales,
4012
+ groupedLocales,
4013
+ localeMapping
4014
+ } = getTargetLocales(allLocales, primaryLocale, requestedTargetLocales);
3949
4015
  if (targetLocales.length === 0) {
4016
+ const skippedMsg = skippedLocales.length > 0 ? ` (Skipped due to Gemini limitation: ${skippedLocales.join(", ")})` : "";
3950
4017
  return {
3951
4018
  content: [
3952
4019
  {
3953
4020
  type: "text",
3954
- text: `\u274C No target locales to translate to. Primary locale: ${primaryLocale}, Available: ${allLocales.join(", ")}`
4021
+ text: `\u274C No target locales to translate to. Primary locale: ${primaryLocale}, Available: ${allLocales.join(", ")}${skippedMsg}`
3955
4022
  }
3956
4023
  ]
3957
4024
  };
3958
4025
  }
3959
- results.push(`\u{1F3AF} Target locales: ${targetLocales.join(", ")}`);
4026
+ results.push(`\u{1F3AF} Target locales to translate: ${targetLocales.join(", ")}`);
4027
+ if (groupedLocales.length > 0) {
4028
+ results.push(
4029
+ `\u{1F4CB} Grouped locales (saved together): ${groupedLocales.join(", ")}`
4030
+ );
4031
+ }
4032
+ if (skippedLocales.length > 0) {
4033
+ results.push(
4034
+ `\u26A0\uFE0F Skipped locales (not supported by Gemini): ${skippedLocales.join(", ")}`
4035
+ );
4036
+ }
3960
4037
  const sourceScreenshots = scanLocaleScreenshots(appInfo.slug, primaryLocale);
3961
4038
  let filteredScreenshots = sourceScreenshots.filter(
3962
4039
  (s) => deviceTypes.includes(s.type)
@@ -4013,6 +4090,7 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4013
4090
  filteredScreenshots,
4014
4091
  primaryLocale,
4015
4092
  targetLocales,
4093
+ localeMapping,
4016
4094
  skipExisting
4017
4095
  );
4018
4096
  if (tasks.length === 0) {
@@ -4041,8 +4119,11 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4041
4119
  tasksByLocale[task.targetLocale].push(task);
4042
4120
  }
4043
4121
  for (const [locale, localeTasks] of Object.entries(tasksByLocale)) {
4122
+ const grouped = localeMapping.get(locale) || [];
4123
+ const groupedOthers = grouped.filter((l) => l !== locale);
4124
+ const groupInfo = groupedOthers.length > 0 ? ` \u2192 also: ${groupedOthers.join(", ")}` : "";
4044
4125
  results.push(`
4045
- \u{1F4C1} ${locale}:`);
4126
+ \u{1F4C1} ${locale}${groupInfo}:`);
4046
4127
  for (const task of localeTasks) {
4047
4128
  results.push(` - ${task.deviceType}/${task.filename}`);
4048
4129
  }
@@ -4098,11 +4179,17 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4098
4179
  if (translationResult.successful > 0) {
4099
4180
  results.push(`
4100
4181
  \u{1F50D} Validating image dimensions...`);
4101
- const successfulTasks = tasks.filter((t) => fs12.existsSync(t.outputPath));
4102
- const resizePairs = successfulTasks.map((t) => ({
4103
- sourcePath: t.sourcePath,
4104
- translatedPath: t.outputPath
4105
- }));
4182
+ const resizePairs = [];
4183
+ for (const task of tasks) {
4184
+ for (const outputPath of task.outputPaths) {
4185
+ if (fs12.existsSync(outputPath)) {
4186
+ resizePairs.push({
4187
+ sourcePath: task.sourcePath,
4188
+ translatedPath: outputPath
4189
+ });
4190
+ }
4191
+ }
4192
+ }
4106
4193
  const resizeResult = await batchValidateAndResize(resizePairs);
4107
4194
  if (resizeResult.resized > 0) {
4108
4195
  results.push(` \u{1F527} Resized ${resizeResult.resized} images to match source dimensions`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-resource-mcp",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",