Skip to content

Commit a9ff96a

Browse files
committed
feat: Add translation functionality to upload command and integrate translate module
1 parent 1af7531 commit a9ff96a

2 files changed

Lines changed: 165 additions & 5 deletions

File tree

src/commands/other/translate.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @fileoverview
3+
* This script is designed to generate SEO-friendly content for a given HTML source.
4+
* It uses the Gemini API to translate and generate content based on the provided HTML and languages.
5+
*
6+
* Prerequisites:
7+
* - Node.js installed on your system.
8+
* - The Google Generative AI library installed:
9+
* `npm install @google/generative-ai`
10+
* - A valid Gemini API key.
11+
*
12+
* Usage:
13+
* Import and call the `translateHtml` function with the HTML source, languages array, and options.
14+
*/
15+
16+
/**
17+
* Example for structured data translation:
18+
*
19+
* {
20+
* "selector": "script[type='application/ld+json']",
21+
* "innerHTML": {
22+
* "en": {
23+
* "@context": "http://schema.org",
24+
* "@type": "WebPage",
25+
* "name": "Basketball Betting Sportsbook | NBA, EuroLeague, NCAA & Global Leagues",
26+
* "description": "Unlock premier basketball betting at Amapola Sportsbook. Get competitive odds for NBA, EuroLeague, NCAA, and global basketball. Enjoy live betting, swift payouts, and insightful picks for an unmatched wagering experience.",
27+
* "url": "https://amapolacasino.com/sportsbook/basketball/",
28+
* "image": "https://amapolacasino.com/assets/basketball-og.jpg",
29+
* "author": {
30+
* "@type": "Organization",
31+
* "name": "AmapolaCasino"
32+
* }
33+
* },
34+
* "es": { ... },
35+
* "fr": { ... },
36+
* "pt": { ... },
37+
* "ht": { ... },
38+
* "nl": { ... },
39+
* "gn": { ... }
40+
* }
41+
* }
42+
*/
43+
44+
const { GoogleGenerativeAI } = require("@google/generative-ai");
45+
const Config = require("@cocreate/config");
46+
const MODEL_NAME = "gemini-2.5-flash-lite";
47+
48+
// Send HTML to Gemini AI and get translation JSON
49+
// Exported function to generate translation object for HTML source and languages
50+
async function getApiKey(options) {
51+
if (options.apiKey) return options.apiKey;
52+
const config = await Config({
53+
GoogleGenerativeAIApiKey: {
54+
prompt: "Enter your Google Generative AI API key: "
55+
}
56+
});
57+
return config.GoogleGenerativeAIApiKey;
58+
}
59+
60+
module.exports = async function translateHtml(html, languages, options = {}) {
61+
const apiKey = await getApiKey(options);
62+
if (!apiKey)
63+
throw new Error(
64+
"Google Generative AI API key is required in options.apiKey, process.env, or via prompt."
65+
);
66+
const genAI = new GoogleGenerativeAI(apiKey);
67+
const model =
68+
options.model || genAI.getGenerativeModel({ model: MODEL_NAME });
69+
const translationObj = await generateTranslationObject(
70+
html,
71+
model,
72+
languages
73+
);
74+
return translationObj;
75+
};
76+
77+
// Update generateTranslationObject to accept only html, model, languages
78+
async function generateTranslationObject(html, model, languages) {
79+
const langList = languages.map((l) => `"${l}"`).join(", ");
80+
const prompt = `
81+
You are an expert web localization AI. Given the following HTML file, extract all translatable content (titles, meta tags, headers, buttons, video/image alt/title, labels, aria-label, and all aria-* attributes) and generate a JSON object in the following format:
82+
83+
{
84+
"translations": [
85+
{
86+
"selector": "<css selector>",
87+
"innerHTML": {
88+
${languages
89+
.map((l) => `\"${l}\"`)
90+
.join(
91+
", \
92+
"
93+
)}
94+
}
95+
},
96+
{
97+
"selector": "<css selector>",
98+
"attributes": {
99+
"alt": { ${langList} },
100+
"label": { ${langList} },
101+
"aria-label": { ${langList} },
102+
"aria-*": { ${langList} },
103+
"title": { ${langList} }
104+
}
105+
},
106+
// Example for structured data translation:
107+
{
108+
"selector": "script[type='application/ld+json']",
109+
"innerHTML": {
110+
"en": { "@context": "http://schema.org", "@type": "WebPage", "name": "English name", "description": "English description" },
111+
"es": { "@context": "http://schema.org", "@type": "WebPage", "name": "Spanish name", "description": "Spanish description" },
112+
"fr": { "@context": "http://schema.org", "@type": "WebPage", "name": "French name", "description": "French description" }
113+
// ...other languages
114+
}
115+
}
116+
// ...more selectors as needed
117+
]
118+
}
119+
120+
Do not add any extra keys or key names not shown in this structure. Only use the keys: name, directory, path, content-type, translations, selector, innerHTML, attributes, alt, label, aria-label, aria-*, title, and the language codes (${languages.join(
121+
", "
122+
)}).
123+
124+
For every translatable item (innerHTML and attributes), provide a translation for each language: ${languages.join(
125+
", "
126+
)}. Do not leave any language blank. For Guarani (\"gn\"), always translate to Guarani and never leave it in English.
127+
128+
Only output the JSON object, do not include any explanation or extra text.
129+
130+
HTML:
131+
${html}
132+
`;
133+
134+
try {
135+
const result = await model.generateContent(prompt);
136+
let jsonText = result.response.candidates[0].content.parts[0].text;
137+
jsonText = jsonText
138+
.replace(/^```json\s*([\s\S]*?)\s*```$/i, "$1")
139+
.trim();
140+
return JSON.parse(jsonText);
141+
} catch (err) {
142+
console.error(`AI error:`, err);
143+
return null;
144+
}
145+
}

src/commands/upload.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@ const file = require("@cocreate/file");
22
const path = require("path");
33
const fs = require("fs");
44
const { getConfig } = require("../getConfig");
5+
const translate = require("./other/translate");
56

67
module.exports = async function upload(directory, args) {
78
if (args && !Array.isArray(args)) args = [args];
89

910
let isWatch = false;
11+
let translateFn = null;
12+
13+
if (args && (args.includes("-t") || args.includes("--translate"))) {
14+
translateFn = translate;
15+
args = args.filter((arg) => arg !== "-t" && arg !== "--translate");
16+
}
17+
1018
if (directory && typeof directory === "string") {
1119
if (["-w", "--watch"].includes(directory)) {
1220
isWatch = true;
@@ -15,7 +23,10 @@ module.exports = async function upload(directory, args) {
1523

1624
directory = process.cwd();
1725

18-
if (isWatch || args.includes("-w") || args.includes("--watch")) {
26+
if (
27+
isWatch ||
28+
(args && (args.includes("-w") || args.includes("--watch")))
29+
) {
1930
for (let i = 0; i < args.length; i++) {
2031
if (args[i].startsWith("-")) continue;
2132
else if (path.isAbsolute(args[i])) directory = args[i];
@@ -30,7 +41,9 @@ module.exports = async function upload(directory, args) {
3041
if (!filename.includes("CoCreate.config.js")) {
3142
const config = await getConfig(directory, filename);
3243
if (config.configPath) {
33-
await file(config, config.configPath, config.filePath);
44+
await file(config, config.configPath, config.filePath, {
45+
translate: translateFn
46+
});
3447
} else {
3548
console.log(
3649
"Failed to read or parse CoCreate.config.js."
@@ -40,13 +53,14 @@ module.exports = async function upload(directory, args) {
4053
}
4154
);
4255
} else {
43-
if (!args.length) {
56+
if (!args || !args.length) {
4457
const CoCreateConfig = await getConfig(directory);
4558
if (CoCreateConfig.configPath) {
4659
await file(
4760
CoCreateConfig,
4861
CoCreateConfig.configPath,
49-
CoCreateConfig.filePath
62+
CoCreateConfig.filePath,
63+
{ translate: translateFn }
5064
);
5165
} else {
5266
console.log("Failed to read or parse CoCreate.config.js.");
@@ -66,7 +80,8 @@ module.exports = async function upload(directory, args) {
6680
await file(
6781
CoCreateConfig,
6882
CoCreateConfig.configPath,
69-
CoCreateConfig.filePath
83+
CoCreateConfig.filePath,
84+
{ translate: translateFn }
7085
);
7186
} else {
7287
console.log(

0 commit comments

Comments
 (0)