pabal-resource-mcp 1.5.3 → 1.5.4

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.
@@ -3523,7 +3523,7 @@ function calculateAspectRatio(width, height) {
3523
3523
  if (Math.abs(ratio - 4 / 3) < 0.1) return "4:3";
3524
3524
  return ratio < 1 ? "9:16" : "16:9";
3525
3525
  }
3526
- async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath) {
3526
+ async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath, preserveWords) {
3527
3527
  try {
3528
3528
  const client = getGeminiClient();
3529
3529
  const sourceLanguage = getLanguageName(sourceLocale);
@@ -3531,6 +3531,8 @@ async function translateImage(sourcePath, sourceLocale, targetLocale, outputPath
3531
3531
  const { width, height } = await getImageDimensions(sourcePath);
3532
3532
  const aspectRatio = calculateAspectRatio(width, height);
3533
3533
  const { data: imageData, mimeType } = readImageAsBase64(sourcePath);
3534
+ const preserveInstruction = preserveWords && preserveWords.length > 0 ? `
3535
+ - Do NOT translate these words, keep them exactly as-is: ${preserveWords.join(", ")}` : "";
3534
3536
  const prompt = `This is an app screenshot with text in ${sourceLanguage}.
3535
3537
  Please translate ONLY the text/words in this image to ${targetLanguage}.
3536
3538
 
@@ -3540,7 +3542,7 @@ IMPORTANT INSTRUCTIONS:
3540
3542
  - Maintain the same font style and text positioning as much as possible
3541
3543
  - Do NOT add any new elements or remove existing design elements
3542
3544
  - The output should look identical except the text language is ${targetLanguage}
3543
- - Preserve all icons, images, and graphical elements exactly as they are`;
3545
+ - Preserve all icons, images, and graphical elements exactly as they are${preserveInstruction}`;
3544
3546
  const chat = client.chats.create({
3545
3547
  model: "gemini-3-pro-image-preview",
3546
3548
  config: {
@@ -3604,7 +3606,7 @@ IMPORTANT INSTRUCTIONS:
3604
3606
  };
3605
3607
  }
3606
3608
  }
3607
- async function translateImagesWithProgress(translations, onProgress) {
3609
+ async function translateImagesWithProgress(translations, onProgress, preserveWords) {
3608
3610
  let successful = 0;
3609
3611
  let failed = 0;
3610
3612
  const errors = [];
@@ -3626,7 +3628,8 @@ async function translateImagesWithProgress(translations, onProgress) {
3626
3628
  translation.sourcePath,
3627
3629
  translation.sourceLocale,
3628
3630
  translation.targetLocale,
3629
- translation.outputPath
3631
+ translation.outputPath,
3632
+ preserveWords
3630
3633
  );
3631
3634
  if (result.success) {
3632
3635
  successful++;
@@ -3659,7 +3662,7 @@ async function getImageDimensions2(imagePath) {
3659
3662
  height: metadata.height
3660
3663
  };
3661
3664
  }
3662
- async function detectEdgeColor(imagePath) {
3665
+ async function detectCornerColor(imagePath) {
3663
3666
  const image = sharp2(imagePath);
3664
3667
  const metadata = await image.metadata();
3665
3668
  const width = metadata.width || 100;
@@ -3667,14 +3670,17 @@ async function detectEdgeColor(imagePath) {
3667
3670
  const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
3668
3671
  const channels = info.channels;
3669
3672
  const colorCounts = /* @__PURE__ */ new Map();
3670
- const sampleEdgePixel = (x, y) => {
3673
+ const cornerWidth = Math.min(100, Math.max(10, Math.floor(width * 0.05)));
3674
+ const cornerHeight = Math.min(100, Math.max(10, Math.floor(height * 0.05)));
3675
+ const samplePixel = (x, y) => {
3676
+ if (x < 0 || x >= width || y < 0 || y >= height) return;
3671
3677
  const idx = (y * width + x) * channels;
3672
3678
  const r = data[idx];
3673
3679
  const g = data[idx + 1];
3674
3680
  const b = data[idx + 2];
3675
- const qr = Math.round(r / 16) * 16;
3676
- const qg = Math.round(g / 16) * 16;
3677
- const qb = Math.round(b / 16) * 16;
3681
+ const qr = Math.round(r / 8) * 8;
3682
+ const qg = Math.round(g / 8) * 8;
3683
+ const qb = Math.round(b / 8) * 8;
3678
3684
  const key = `${qr},${qg},${qb}`;
3679
3685
  const existing = colorCounts.get(key);
3680
3686
  if (existing) {
@@ -3683,13 +3689,22 @@ async function detectEdgeColor(imagePath) {
3683
3689
  colorCounts.set(key, { count: 1, color: { r: qr, g: qg, b: qb } });
3684
3690
  }
3685
3691
  };
3686
- for (let x = 0; x < width; x += 2) {
3687
- sampleEdgePixel(x, 0);
3688
- sampleEdgePixel(x, height - 1);
3689
- }
3690
- for (let y = 0; y < height; y += 2) {
3691
- sampleEdgePixel(0, y);
3692
- sampleEdgePixel(width - 1, y);
3692
+ const corners = [
3693
+ { startX: 0, startY: 0 },
3694
+ // Top-left
3695
+ { startX: width - cornerWidth, startY: 0 },
3696
+ // Top-right
3697
+ { startX: 0, startY: height - cornerHeight },
3698
+ // Bottom-left
3699
+ { startX: width - cornerWidth, startY: height - cornerHeight }
3700
+ // Bottom-right
3701
+ ];
3702
+ for (const corner of corners) {
3703
+ for (let y = corner.startY; y < corner.startY + cornerHeight; y += 2) {
3704
+ for (let x = corner.startX; x < corner.startX + cornerWidth; x += 2) {
3705
+ samplePixel(x, y);
3706
+ }
3707
+ }
3693
3708
  }
3694
3709
  let maxCount = 0;
3695
3710
  let dominantColor = { r: 255, g: 255, b: 255 };
@@ -3702,7 +3717,7 @@ async function detectEdgeColor(imagePath) {
3702
3717
  return dominantColor;
3703
3718
  }
3704
3719
  async function resizeImage(inputPath, outputPath, targetDimensions) {
3705
- const bgColor = await detectEdgeColor(inputPath);
3720
+ const bgColor = await detectCornerColor(inputPath);
3706
3721
  await sharp2(inputPath).resize(targetDimensions.width, targetDimensions.height, {
3707
3722
  fit: "contain",
3708
3723
  // Preserve aspect ratio
@@ -3763,8 +3778,17 @@ var localizeScreenshotsInputSchema = z7.object({
3763
3778
  deviceTypes: z7.array(z7.enum(["phone", "tablet"])).optional().default(["phone", "tablet"]).describe("Device types to process (default: both phone and tablet)"),
3764
3779
  dryRun: z7.boolean().optional().default(false).describe("Preview mode - shows what would be translated without actually translating"),
3765
3780
  skipExisting: z7.boolean().optional().default(true).describe("Skip translation if target file already exists (default: true)"),
3766
- screenshotNumbers: z7.array(z7.number().int().positive()).optional().describe(
3767
- "Specific screenshot numbers to process (e.g., [1, 3, 5]). If not provided, all screenshots will be processed."
3781
+ screenshotNumbers: z7.union([
3782
+ z7.array(z7.number().int().positive()),
3783
+ z7.object({
3784
+ phone: z7.array(z7.number().int().positive()).optional(),
3785
+ tablet: z7.array(z7.number().int().positive()).optional()
3786
+ })
3787
+ ]).optional().describe(
3788
+ "Specific screenshot numbers to process. Can be:\n- Array for all devices: [1, 3, 5]\n- Object for per-device: { phone: [1, 2], tablet: [1, 3, 5] }\nIf not provided, all screenshots will be processed."
3789
+ ),
3790
+ preserveWords: z7.array(z7.string()).optional().describe(
3791
+ 'Words to keep untranslated (e.g., brand names, product names). Example: ["Pabal", "Pro", "AI"]'
3768
3792
  )
3769
3793
  });
3770
3794
  var jsonSchema7 = zodToJsonSchema7(localizeScreenshotsInputSchema, {
@@ -3881,7 +3905,8 @@ async function handleLocalizeScreenshots(input) {
3881
3905
  deviceTypes = ["phone", "tablet"],
3882
3906
  dryRun = false,
3883
3907
  skipExisting = true,
3884
- screenshotNumbers
3908
+ screenshotNumbers,
3909
+ preserveWords
3885
3910
  } = input;
3886
3911
  const results = [];
3887
3912
  let appInfo;
@@ -3936,16 +3961,34 @@ async function handleLocalizeScreenshots(input) {
3936
3961
  let filteredScreenshots = sourceScreenshots.filter(
3937
3962
  (s) => deviceTypes.includes(s.type)
3938
3963
  );
3939
- if (screenshotNumbers && screenshotNumbers.length > 0) {
3964
+ if (screenshotNumbers) {
3965
+ const isArray = Array.isArray(screenshotNumbers);
3966
+ const phoneNumbers = isArray ? screenshotNumbers : screenshotNumbers.phone;
3967
+ const tabletNumbers = isArray ? screenshotNumbers : screenshotNumbers.tablet;
3940
3968
  filteredScreenshots = filteredScreenshots.filter((s) => {
3941
3969
  const match = s.filename.match(/^(\d+)\./);
3942
- if (match) {
3943
- const num = parseInt(match[1], 10);
3944
- return screenshotNumbers.includes(num);
3970
+ if (!match) return false;
3971
+ const num = parseInt(match[1], 10);
3972
+ const numbersForDevice = s.type === "phone" ? phoneNumbers : tabletNumbers;
3973
+ if (!numbersForDevice || numbersForDevice.length === 0) {
3974
+ return true;
3945
3975
  }
3946
- return false;
3976
+ return numbersForDevice.includes(num);
3947
3977
  });
3948
- results.push(`\u{1F522} Filtering screenshots: ${screenshotNumbers.join(", ")}`);
3978
+ const filterParts = [];
3979
+ if (isArray) {
3980
+ filterParts.push(`all: ${screenshotNumbers.join(", ")}`);
3981
+ } else {
3982
+ if (phoneNumbers && phoneNumbers.length > 0) {
3983
+ filterParts.push(`phone: ${phoneNumbers.join(", ")}`);
3984
+ }
3985
+ if (tabletNumbers && tabletNumbers.length > 0) {
3986
+ filterParts.push(`tablet: ${tabletNumbers.join(", ")}`);
3987
+ }
3988
+ }
3989
+ if (filterParts.length > 0) {
3990
+ results.push(`\u{1F522} Filtering screenshots: ${filterParts.join(" | ")}`);
3991
+ }
3949
3992
  }
3950
3993
  if (filteredScreenshots.length === 0) {
3951
3994
  const screenshotsDir2 = getScreenshotsDir(appInfo.slug);
@@ -4015,6 +4058,9 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4015
4058
  }
4016
4059
  results.push(`
4017
4060
  \u{1F680} Starting translations...`);
4061
+ if (preserveWords && preserveWords.length > 0) {
4062
+ results.push(`\u{1F512} Preserving words: ${preserveWords.join(", ")}`);
4063
+ }
4018
4064
  const translationResult = await translateImagesWithProgress(
4019
4065
  tasks,
4020
4066
  (progress) => {
@@ -4032,7 +4078,8 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
4032
4078
  `\u274C ${progressPrefix} ${progress.targetLocale}/${progress.deviceType}/${progress.filename}: ${progress.error}`
4033
4079
  );
4034
4080
  }
4035
- }
4081
+ },
4082
+ preserveWords
4036
4083
  );
4037
4084
  results.push(`
4038
4085
  \u{1F4CA} Translation Results:`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-resource-mcp",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",