Skip to content

Commit ffacde4

Browse files
committed
feat(translations): added translation buttons to FAQs, categories and custom pages (#3720)
1 parent a15453c commit ffacde4

File tree

24 files changed

+373
-68
lines changed

24 files changed

+373
-68
lines changed

phpmyfaq/admin/assets/src/api/configuration.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ export const fetchSeoMetaTags = async (currentValue: string): Promise<string> =>
9999
return await response.text();
100100
};
101101

102+
export const fetchTranslationProvider = async (currentValue: string): Promise<string> => {
103+
const response = await fetch(`./api/configuration/translation-provider/${currentValue}`);
104+
105+
if (!response.ok) {
106+
return '';
107+
}
108+
109+
return await response.text();
110+
};
111+
102112
export const fetchTemplates = async (): Promise<string> => {
103113
const response = await fetch(`./api/configuration/templates`);
104114

phpmyfaq/admin/assets/src/configuration/configuration.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
fetchSeoMetaTags,
2727
fetchTemplates,
2828
fetchTranslations,
29+
fetchTranslationProvider,
2930
saveConfiguration,
3031
} from '../api';
3132
import { Response } from '../interfaces';
@@ -69,6 +70,9 @@ export const handleConfiguration = async (): Promise<void> => {
6970
case '#mail':
7071
await handleSMTPPasswordToggle();
7172
break;
73+
case '#translation':
74+
await handleTranslationProvider();
75+
break;
7276
}
7377

7478
tabLoaded = true;
@@ -240,6 +244,17 @@ export const handleSeoMetaTags = async (): Promise<void> => {
240244
}
241245
};
242246

247+
export const handleTranslationProvider = async (): Promise<void> => {
248+
const translationProviderSelectBox = document.getElementsByName(
249+
'edit[translation.provider]'
250+
) as NodeListOf<HTMLSelectElement>;
251+
if (translationProviderSelectBox !== null && translationProviderSelectBox[0]) {
252+
const currentValue = (translationProviderSelectBox[0].dataset.pmfConfigurationCurrentValue as string) || 'none';
253+
const options = await fetchTranslationProvider(currentValue);
254+
translationProviderSelectBox[0].insertAdjacentHTML('beforeend', options);
255+
}
256+
};
257+
243258
export const handleConfigurationTab = async (target: string): Promise<void> => {
244259
const languageElement = document.getElementById('pmf-language') as HTMLInputElement;
245260
if (!languageElement) {

phpmyfaq/admin/assets/src/content/category.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Modal } from 'bootstrap';
1818
import { deleteCategory, setCategoryTree } from '../api';
1919
import { pushErrorNotification, pushNotification } from '../../../../assets/src/utils';
2020
import { Response } from '../interfaces';
21+
import { Translator } from '../translation/translator';
2122

2223
const nestedQuery = '.nested-sortable';
2324
const identifier = 'pmfCatid';
@@ -46,7 +47,7 @@ export const handleCategories = (): void => {
4647
});
4748
},
4849
onEnd: async (event: SortableEvent): Promise<void> => {
49-
// Remove class from all drop zones when drag ends
50+
// Remove the class from all drop zones when drag ends
5051
const allSortables = document.querySelectorAll<HTMLElement>(nestedQuery);
5152
allSortables.forEach((sortable: HTMLElement): void => {
5253
sortable.classList.remove('sortable-drag-active');
@@ -133,3 +134,52 @@ export const handleResetCategoryImage = (): void => {
133134
});
134135
}
135136
};
137+
138+
export const handleCategoryTranslate = (): void => {
139+
const translateButton = document.getElementById('btn-translate-category-ai') as HTMLButtonElement | null;
140+
const langSelect = document.getElementById('catlang') as HTMLSelectElement | null;
141+
const originalLangInput = document.getElementById('originalCategoryLang') as HTMLInputElement | null;
142+
143+
if (!translateButton || !langSelect || !originalLangInput) {
144+
return;
145+
}
146+
147+
// Initialize translator when target language is selected
148+
langSelect.addEventListener('change', () => {
149+
const sourceLang = originalLangInput.value;
150+
const targetLang = langSelect.value;
151+
152+
if (sourceLang && targetLang && sourceLang !== targetLang) {
153+
// Enable the translate button
154+
translateButton.disabled = false;
155+
156+
// Initialize the Translator
157+
try {
158+
new Translator({
159+
buttonSelector: '#btn-translate-category-ai',
160+
contentType: 'category',
161+
sourceLang: sourceLang,
162+
targetLang: targetLang,
163+
fieldMapping: {
164+
name: '#name',
165+
description: '#description',
166+
},
167+
onTranslationSuccess: () => {
168+
pushNotification('Translation completed successfully');
169+
},
170+
onTranslationError: (error) => {
171+
pushErrorNotification(`Translation failed: ${error}`);
172+
},
173+
});
174+
} catch (error) {
175+
console.error('Failed to initialize translator:', error);
176+
}
177+
} else {
178+
// Disable the translate button if same language or no target language
179+
translateButton.disabled = true;
180+
}
181+
});
182+
183+
// Initially disable the button
184+
translateButton.disabled = true;
185+
};

phpmyfaq/admin/assets/src/content/faqs.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
*/
1515

1616
import { deleteAttachments } from '../api';
17-
import { pushNotification } from '../../../../assets/src/utils';
17+
import { pushNotification, pushErrorNotification } from '../../../../assets/src/utils';
18+
import { Translator } from '../translation/translator';
1819

1920
const showHelp = (option: string): void => {
2021
const optionHelp = document.getElementById(`${option}Help`) as HTMLElement;
@@ -143,3 +144,53 @@ const checkForHash = (): void => {
143144
submitButton.removeAttribute('disabled');
144145
}
145146
};
147+
148+
export const handleFaqTranslate = (): void => {
149+
const translateButton = document.getElementById('btn-translate-faq-ai') as HTMLButtonElement | null;
150+
const langSelect = document.getElementById('lang') as HTMLSelectElement | null;
151+
const originalLangInput = document.getElementById('originalFaqLang') as HTMLInputElement | null;
152+
153+
if (!translateButton || !langSelect || !originalLangInput) {
154+
return;
155+
}
156+
157+
// Initialize translator when target language is selected
158+
langSelect.addEventListener('change', () => {
159+
const sourceLang = originalLangInput.value;
160+
const targetLang = langSelect.value;
161+
162+
if (sourceLang && targetLang && sourceLang !== targetLang) {
163+
// Enable the translate button
164+
translateButton.disabled = false;
165+
166+
// Initialize the Translator
167+
try {
168+
new Translator({
169+
buttonSelector: '#btn-translate-faq-ai',
170+
contentType: 'faq',
171+
sourceLang: sourceLang,
172+
targetLang: targetLang,
173+
fieldMapping: {
174+
question: '#question',
175+
answer: '#editor',
176+
keywords: '#keywords',
177+
},
178+
onTranslationSuccess: () => {
179+
pushNotification('Translation completed successfully');
180+
},
181+
onTranslationError: (error) => {
182+
pushErrorNotification(`Translation failed: ${error}`);
183+
},
184+
});
185+
} catch (error) {
186+
console.error('Failed to initialize translator:', error);
187+
}
188+
} else {
189+
// Disable the translate button if same language or no target language
190+
translateButton.disabled = true;
191+
}
192+
});
193+
194+
// Initially disable the button
195+
translateButton.disabled = true;
196+
};

phpmyfaq/admin/assets/src/content/pages.ts

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Modal } from 'bootstrap';
1818
import { pushErrorNotification, pushNotification } from '../../../../assets/src/utils';
1919
import { Response } from '../interfaces';
2020
import { renderPageEditor } from './editor';
21+
import { Translator } from '../translation/translator';
2122

2223
interface PageData {
2324
pageTitle: string;
@@ -193,7 +194,7 @@ export const handleTranslatePage = (): void => {
193194
const form = document.getElementById('pmf-translate-page-form') as HTMLFormElement | null;
194195
if (!form) return;
195196

196-
// Initialize WYSIWYG editor for content field
197+
// Initialize WYSIWYG editor for the content field
197198
renderPageEditor();
198199

199200
const titleInput = document.getElementById('pageTitle') as HTMLInputElement | null;
@@ -246,6 +247,53 @@ export const handleTranslatePage = (): void => {
246247
updateCharCounter('seoTitle', 'seo-title-counter', 60);
247248
updateCharCounter('seoDescription', 'seo-description-counter', 160);
248249

250+
// AI Translation integration
251+
const translateButton = document.getElementById('btn-translate-page-ai') as HTMLButtonElement | null;
252+
const originalLangInput = document.getElementById('originalLang') as HTMLInputElement | null;
253+
254+
if (translateButton && langInput && originalLangInput) {
255+
// Initialize translator when the target language is selected
256+
langInput.addEventListener('change', () => {
257+
const sourceLang = originalLangInput.value;
258+
const targetLang = langInput.value;
259+
260+
if (sourceLang && targetLang && sourceLang !== targetLang) {
261+
// Enable the translate button
262+
translateButton.disabled = false;
263+
264+
// Initialize the Translator
265+
try {
266+
new Translator({
267+
buttonSelector: '#btn-translate-page-ai',
268+
contentType: 'customPage',
269+
sourceLang: sourceLang,
270+
targetLang: targetLang,
271+
fieldMapping: {
272+
pageTitle: '#pageTitle',
273+
content: '#content',
274+
seoTitle: '#seoTitle',
275+
seoDescription: '#seoDescription',
276+
},
277+
onTranslationSuccess: () => {
278+
pushNotification('Translation completed successfully');
279+
},
280+
onTranslationError: (error) => {
281+
pushErrorNotification(`Translation failed: ${error}`);
282+
},
283+
});
284+
} catch (error) {
285+
console.error('Failed to initialize translator:', error);
286+
}
287+
} else {
288+
// Disable the translation button if the same language or no target language
289+
translateButton.disabled = true;
290+
}
291+
});
292+
293+
// Initially disable the button
294+
translateButton.disabled = true;
295+
}
296+
249297
// Form submission
250298
const submitButton = document.getElementById('pmf-submit-page') as HTMLButtonElement | null;
251299
if (submitButton) {
@@ -268,14 +316,10 @@ export const handleTranslatePage = (): void => {
268316
};
269317

270318
const response = (await addPage(data)) as unknown as Response;
271-
if (typeof response.success === 'string') {
272-
pushNotification(response.success);
273-
setTimeout(() => {
274-
window.location.href = './pages';
275-
}, 2000);
276-
} else {
277-
pushErrorNotification(response.error || 'An error occurred');
278-
}
319+
pushNotification(response.success);
320+
setTimeout(() => {
321+
window.location.href = './pages';
322+
}, 2000);
279323
});
280324
}
281325
};
@@ -287,7 +331,7 @@ export const handleEditPage = (): void => {
287331
const form = document.getElementById('pmf-edit-page-form') as HTMLFormElement | null;
288332
if (!form) return;
289333

290-
// Initialize WYSIWYG editor for content field
334+
// Initialize WYSIWYG editor for the content field
291335
renderPageEditor();
292336

293337
const slugInput = document.getElementById('slug') as HTMLInputElement | null;
@@ -347,14 +391,10 @@ export const handleEditPage = (): void => {
347391
};
348392

349393
const response = (await updatePage(data)) as unknown as Response;
350-
if (typeof response.success === 'string') {
351-
pushNotification(response.success);
352-
setTimeout(() => {
353-
window.location.href = './pages';
354-
}, 2000);
355-
} else {
356-
pushErrorNotification(response.error || 'An error occurred');
357-
}
394+
pushNotification(response.success);
395+
setTimeout(() => {
396+
window.location.href = './pages';
397+
}, 2000);
358398
});
359399
}
360400
};
@@ -388,14 +428,10 @@ export const handlePages = (): void => {
388428
const pageLang = (document.getElementById('pageLang') as HTMLInputElement).value;
389429

390430
const response = (await deletePage(csrfToken, pageId, pageLang)) as unknown as Response;
391-
if (typeof response.success === 'string') {
392-
pushNotification(response.success);
393-
setTimeout(() => {
394-
window.location.reload();
395-
}, 2000);
396-
} else {
397-
pushErrorNotification(response.error || 'An error occurred');
398-
}
431+
pushNotification(response.success);
432+
setTimeout(() => {
433+
window.location.reload();
434+
}, 2000);
399435
});
400436
}
401437
}
@@ -410,12 +446,7 @@ export const handlePages = (): void => {
410446

411447
const response = (await activatePage(pageId, status, csrfToken)) as unknown as Response;
412448

413-
if (typeof response.success === 'string') {
414-
pushNotification(response.success);
415-
} else {
416-
pushErrorNotification(response.error || 'An error occurred');
417-
checkbox.checked = !status;
418-
}
449+
pushNotification(response.success);
419450
});
420451
});
421452
};

phpmyfaq/admin/assets/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import {
7171
handleResetCategoryImage,
7272
handleResetButton,
7373
handleDeleteFaqEditorModal,
74+
handleFaqTranslate,
75+
handleCategoryTranslate,
7476
} from './content';
7577
import { handleUserList, handleUsers } from './user';
7678
import { handleGroups } from './group';
@@ -107,11 +109,13 @@ document.addEventListener('DOMContentLoaded', async (): Promise<void> => {
107109
// Content → Categories
108110
handleCategories();
109111
handleResetCategoryImage();
112+
handleCategoryTranslate();
110113
await handleCategoryDelete();
111114

112115
// Content → add/edit FAQs
113116
renderEditor();
114117
handleFaqForm();
118+
handleFaqTranslate();
115119
handleMarkdownForm();
116120
handleAttachmentUploads();
117121
handleFileFilter();

0 commit comments

Comments
 (0)