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.
- package/dist/bin/mcp-server.js +119 -12
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -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: "
|
|
3665
|
-
//
|
|
3666
|
-
withoutEnlargement: false
|
|
3722
|
+
fit: "contain",
|
|
3723
|
+
// Preserve aspect ratio
|
|
3724
|
+
withoutEnlargement: false,
|
|
3667
3725
|
// Allow enlargement if needed
|
|
3668
|
-
|
|
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
|
-
|
|
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:`);
|