document-translation / document-authoring.js
dmartincy's picture
Add translation pairs
71aa48d
Raw
History Blame
8.14 kB
// Initialize the app container.
const app = document.getElementById('app');
app.innerHTML = `
<div style="height: 100vh; display: flex; flex-direction: column;">
<div style="background: #f5f5f5; border-bottom: 1px solid #ddd; padding: 10px;">
<div id="statusIndicator">
<span class="spinner"></span> Translation services starting...
</div>
<div id="translationControls">
<select id="sourceLanguageSelect">
<option value="English" selected>English</option>
<option value="Spanish">Spanish</option>
<option value="French">French</option>
<option value="German">German</option>
</select>
<span style="margin: 0 10px;">→</span>
<select id="targetLanguageSelect">
<option value="English">English</option>
<option value="Spanish">Spanish</option>
<option value="French">French</option>
<option value="German">German</option>
</select>
<button id="translateButton">Translate Document</button>
</div>
</div>
<div style="flex: 1; display: flex; flex-direction: column; position: relative;">
<div id="editor" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
</div>
</div>
`;
// Add retry count
let retryCount = 0;
const MAX_RETRIES = 10; // Will try for 20 seconds (10 attempts * 2 second interval)
function checkServicesStatus() {
fetch('/healthcheck')
.then(response => {
if (response.ok) {
window.servicesReady = true;
const translationControls = document.getElementById('translationControls');
const statusIndicator = document.getElementById('statusIndicator');
if (translationControls) {
translationControls.style.display = 'block';
}
if (statusIndicator) {
statusIndicator.style.display = 'none';
}
return true;
}
throw new Error('Services not ready');
})
.catch(error => {
retryCount++;
const statusIndicator = document.getElementById('statusIndicator');
if (statusIndicator) {
if (retryCount >= MAX_RETRIES) {
statusIndicator.innerHTML = '❌ Failed to initialize translation services. Try restarting the space.';
statusIndicator.style.color = '#dc3545';
clearInterval(statusInterval);
} else {
statusIndicator.innerHTML = `<span class="spinner"></span> Translation services starting... (attempt ${retryCount}/${MAX_RETRIES})`;
}
}
console.log('Waiting for services...', error);
return false;
});
}
// Check status every 2 seconds until ready or max retries reached
const statusInterval = setInterval(() => {
if (window.servicesReady || retryCount >= MAX_RETRIES) {
clearInterval(statusInterval);
} else {
checkServicesStatus();
}
}, 2000);
// Load Document Authoring SDK
const script = document.createElement('script');
script.src = 'https://document-authoring-cdn.pspdfkit.com/releases/document-authoring-1.0.26-umd.js';
script.crossOrigin = true;
document.head.appendChild(script);
script.onload = async () => {
const docAuthSystem = await DocAuth.createDocAuthSystem({});
const editor = await docAuthSystem.createEditor(
document.getElementById("editor"),
{
document: await docAuthSystem.importDOCX(fetch('./Sample.docx')),
}
);
// Helper function to extract text runs with their paths
function flattenTextRuns(obj, path = [], result = []) {
for (let key in obj) {
const newPath = [...path, key];
if (typeof obj[key] === "string" && key === "text" && obj[key].length > 0) {
result.push({ text: obj[key], path: newPath });
} else if (typeof obj[key] === "object") {
flattenTextRuns(obj[key], newPath, result);
}
}
return result;
}
// Helper function to group adjacent text runs
function groupAdjacentRuns(textRuns, maxChunkSize = 1000) {
const groups = [];
let currentGroup = [];
let currentSize = 0;
for (let run of textRuns) {
if (currentSize + run.text.length > maxChunkSize && currentGroup.length > 0) {
groups.push([...currentGroup]);
currentGroup = [];
currentSize = 0;
}
currentGroup.push(run);
currentSize += run.text.length;
}
if (currentGroup.length > 0) {
groups.push(currentGroup);
}
return groups;
}
async function translate(content, targetLang, sourceLang = 'English') {
try {
const response = await fetch('/client/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: [
{
role: "system",
content: `Translate the following text from ${sourceLang} to ${targetLang}. Each segment is marked with ||| delimiters. Translate each segment independently while preserving original capitalization and punctuation. Add spaces at the start or end of segments only if they exist in the original text.`
},
{
role: "user",
content: content
}
],
model: "gemma-2b",
stream: false
})
});
if (!response.ok) {
throw new Error('Translation request failed');
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
console.error("Translation error:", error);
throw new Error("Failed to generate translation");
}
}
// Function to update nested object value by path
function setNestedValue(obj, path, value) {
let current = obj;
for (let i = 0; i < path.length - 1; i++) {
current = current[path[i]];
}
current[path[path.length - 1]] = value;
}
// Enhanced recursive translation function
async function translateRecursive(docJson, targetLang, sourceLang = 'English') {
const textRuns = flattenTextRuns(docJson);
const groups = groupAdjacentRuns(textRuns);
for (let group of groups) {
const content = group.map(run => `|||${run.text}|||`).join('\n');
const translatedContent = await translate(content, targetLang, sourceLang);
const translations = translatedContent
.split('|||')
.filter(t => t.trim())
.map(t => t.trim());
group.forEach((run, index) => {
if (translations[index]) {
const originalText = run.text;
let translatedText = translations[index];
if (originalText.startsWith(' ')) {
translatedText = ' ' + translatedText;
}
if (originalText.endsWith(' ')) {
translatedText = translatedText + ' ';
}
setNestedValue(docJson, run.path, translatedText);
}
});
}
}
// Add translation button handler
const translateButton = document.getElementById('translateButton');
const sourceLanguageSelect = document.getElementById('sourceLanguageSelect');
const targetLanguageSelect = document.getElementById('targetLanguageSelect');
translateButton.addEventListener('click', async () => {
try {
const sourceLanguage = sourceLanguageSelect.value;
const targetLanguage = targetLanguageSelect.value;
if (sourceLanguage === targetLanguage) {
alert('Please select different languages for source and target');
return;
}
translateButton.disabled = true;
translateButton.innerHTML = '<span class="spinner"></span> Translating...';
const jsonDoc = await editor.currentDocument().saveDocument();
await translateRecursive(jsonDoc.container.document, targetLanguage, sourceLanguage);
const translatedDoc = await docAuthSystem.loadDocument(jsonDoc);
editor.setCurrentDocument(translatedDoc);
} catch (error) {
alert('Translation failed: ' + error.message);
} finally {
translateButton.disabled = false;
translateButton.innerHTML = 'Translate Document';
}
});
};