pabal-resource-mcp 1.5.2 → 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,13 +3662,70 @@ async function getImageDimensions2(imagePath) {
3659
3662
  height: metadata.height
3660
3663
  };
3661
3664
  }
3665
+ async function detectCornerColor(imagePath) {
3666
+ const image = sharp2(imagePath);
3667
+ const metadata = await image.metadata();
3668
+ const width = metadata.width || 100;
3669
+ const height = metadata.height || 100;
3670
+ const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
3671
+ const channels = info.channels;
3672
+ const colorCounts = /* @__PURE__ */ new Map();
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;
3677
+ const idx = (y * width + x) * channels;
3678
+ const r = data[idx];
3679
+ const g = data[idx + 1];
3680
+ const b = data[idx + 2];
3681
+ const qr = Math.round(r / 8) * 8;
3682
+ const qg = Math.round(g / 8) * 8;
3683
+ const qb = Math.round(b / 8) * 8;
3684
+ const key = `${qr},${qg},${qb}`;
3685
+ const existing = colorCounts.get(key);
3686
+ if (existing) {
3687
+ existing.count++;
3688
+ } else {
3689
+ colorCounts.set(key, { count: 1, color: { r: qr, g: qg, b: qb } });
3690
+ }
3691
+ };
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
+ }
3708
+ }
3709
+ let maxCount = 0;
3710
+ let dominantColor = { r: 255, g: 255, b: 255 };
3711
+ for (const { count, color } of colorCounts.values()) {
3712
+ if (count > maxCount) {
3713
+ maxCount = count;
3714
+ dominantColor = color;
3715
+ }
3716
+ }
3717
+ return dominantColor;
3718
+ }
3662
3719
  async function resizeImage(inputPath, outputPath, targetDimensions) {
3720
+ const bgColor = await detectCornerColor(inputPath);
3663
3721
  await sharp2(inputPath).resize(targetDimensions.width, targetDimensions.height, {
3664
- fit: "fill",
3665
- // Exact resize to target dimensions
3666
- withoutEnlargement: false
3722
+ fit: "contain",
3723
+ // Preserve aspect ratio
3724
+ withoutEnlargement: false,
3667
3725
  // Allow enlargement if needed
3668
- }).toFile(outputPath + ".tmp");
3726
+ background: bgColor
3727
+ // Use detected edge color
3728
+ }).flatten({ background: bgColor }).png().toFile(outputPath + ".tmp");
3669
3729
  fs11.renameSync(outputPath + ".tmp", outputPath);
3670
3730
  }
3671
3731
  async function validateAndResizeImage(sourcePath, translatedPath) {
@@ -3717,7 +3777,19 @@ var localizeScreenshotsInputSchema = z7.object({
3717
3777
  ),
3718
3778
  deviceTypes: z7.array(z7.enum(["phone", "tablet"])).optional().default(["phone", "tablet"]).describe("Device types to process (default: both phone and tablet)"),
3719
3779
  dryRun: z7.boolean().optional().default(false).describe("Preview mode - shows what would be translated without actually translating"),
3720
- skipExisting: z7.boolean().optional().default(true).describe("Skip translation if target file already exists (default: true)")
3780
+ skipExisting: z7.boolean().optional().default(true).describe("Skip translation if target file already exists (default: true)"),
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"]'
3792
+ )
3721
3793
  });
3722
3794
  var jsonSchema7 = zodToJsonSchema7(localizeScreenshotsInputSchema, {
3723
3795
  name: "LocalizeScreenshotsInput",
@@ -3832,7 +3904,9 @@ async function handleLocalizeScreenshots(input) {
3832
3904
  targetLocales: requestedTargetLocales,
3833
3905
  deviceTypes = ["phone", "tablet"],
3834
3906
  dryRun = false,
3835
- skipExisting = true
3907
+ skipExisting = true,
3908
+ screenshotNumbers,
3909
+ preserveWords
3836
3910
  } = input;
3837
3911
  const results = [];
3838
3912
  let appInfo;
@@ -3884,9 +3958,38 @@ async function handleLocalizeScreenshots(input) {
3884
3958
  }
3885
3959
  results.push(`\u{1F3AF} Target locales: ${targetLocales.join(", ")}`);
3886
3960
  const sourceScreenshots = scanLocaleScreenshots(appInfo.slug, primaryLocale);
3887
- const filteredScreenshots = sourceScreenshots.filter(
3961
+ let filteredScreenshots = sourceScreenshots.filter(
3888
3962
  (s) => deviceTypes.includes(s.type)
3889
3963
  );
3964
+ if (screenshotNumbers) {
3965
+ const isArray = Array.isArray(screenshotNumbers);
3966
+ const phoneNumbers = isArray ? screenshotNumbers : screenshotNumbers.phone;
3967
+ const tabletNumbers = isArray ? screenshotNumbers : screenshotNumbers.tablet;
3968
+ filteredScreenshots = filteredScreenshots.filter((s) => {
3969
+ const match = s.filename.match(/^(\d+)\./);
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;
3975
+ }
3976
+ return numbersForDevice.includes(num);
3977
+ });
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
+ }
3992
+ }
3890
3993
  if (filteredScreenshots.length === 0) {
3891
3994
  const screenshotsDir2 = getScreenshotsDir(appInfo.slug);
3892
3995
  return {
@@ -3955,6 +4058,9 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
3955
4058
  }
3956
4059
  results.push(`
3957
4060
  \u{1F680} Starting translations...`);
4061
+ if (preserveWords && preserveWords.length > 0) {
4062
+ results.push(`\u{1F512} Preserving words: ${preserveWords.join(", ")}`);
4063
+ }
3958
4064
  const translationResult = await translateImagesWithProgress(
3959
4065
  tasks,
3960
4066
  (progress) => {
@@ -3972,7 +4078,8 @@ ${screenshotsDir2}/${primaryLocale}/tablet/1.png, 2.png, ...`
3972
4078
  `\u274C ${progressPrefix} ${progress.targetLocale}/${progress.deviceType}/${progress.filename}: ${progress.error}`
3973
4079
  );
3974
4080
  }
3975
- }
4081
+ },
4082
+ preserveWords
3976
4083
  );
3977
4084
  results.push(`
3978
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.2",
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",