weploy-translate
Version:
Translate your React.js or Next.js app with AI
174 lines (144 loc) • 7.86 kB
JavaScript
const { getGlobalseoOptions, MERGE_PREFIX, setIsTranslationInitialized } = require("../configs");
const { renderSelectorState } = require("../selector/renderSelectorState");
const extractTextNodes = require("./extractTextNodes");
const filterValidTextNodes = require("./filterValidTextNodes");
const { isExcludedClassName, isExcludedId } = require("./isExcluded");
const isUrl = require("./isUrl");
const translateNodes = require("./translateNodes");
function modifyHtmlStrings(window, rootElement, language, apiKey, shouldOptimizeSEO) {
return new Promise((resolve, reject) => {
const seoNodes = []; // document will represent the title tag, if node == document then the updateNode function will update the title
const otherNodes = []; // like form placeholder or other attributes that need to be translated
if (shouldOptimizeSEO) {
const metaTags = Array.from(window.document.getElementsByTagName('meta'));
const cleanMetaTags = metaTags.filter((meta) => {
if (!(meta.content || "").trim()) return false;
const validMetaTagNames = ["description", "og:title", "og:description", "twitter:title", "twitter:description"];
function isValidMeta() {
const metaName = meta.name.toLowerCase();
const metaProperty = (meta.getAttribute('property') || "").toLowerCase();
return validMetaTagNames.includes(metaName) || validMetaTagNames.includes(metaProperty);
}
if (!isValidMeta()) return false;
const isTheContentAnUrl = isUrl(meta.content);
if (isTheContentAnUrl) return false;
return true;
});
const options = getGlobalseoOptions(window);
const imgTags = options.translateAttributes ? Array.from(window.document.getElementsByTagName('img')) : [];
// only include img tags that has alt or title attribute
const cleanImgTags = imgTags.filter((img) =>
(img.alt || "").trim() ||
(img.title || "").trim()
);
const anchorTags = options.translateAttributes ? Array.from(window.document.getElementsByTagName('a')) : [];
// only include anchor tags that has title attribute
const cleanAnchorTags = anchorTags.filter((anchor) => (anchor.title || "").trim());
seoNodes.push(
window.document,
...cleanMetaTags,
...cleanImgTags,
...cleanAnchorTags,
)
}
const options = getGlobalseoOptions(window);
if (options.translateFormPlaceholder) {
const inputTags = Array.from(window.document.getElementsByTagName('input'));
// only include input that has placeholder attribute
const cleanAnchorTags = inputTags.filter((node) => (node.placeholder || "").trim() && !isExcludedClassName(window,node.className) && !isExcludedId(window, node.id));
const textareaTags = Array.from(window.document.getElementsByTagName('textarea'));
// only include textarea that has placeholder attribute
const cleanTextareaTags = textareaTags.filter((node) => (node.placeholder || "").trim() && !isExcludedClassName(window,node.className) && !isExcludedId(window, node.id));
otherNodes.push(
...cleanAnchorTags,
...cleanTextareaTags,
)
}
if (options.translateSelectOptions) {
const optionTags = Array.from(window.document.getElementsByTagName('option'));
// only include option that has value attribute and make sure the select tag is not excluded
const cleanOptionTags = optionTags.filter((node) => (node.textContent || "").trim() && !isExcludedClassName(window,node.className) && node.parentElement.tagName == "SELECT" && !isExcludedClassName(window,node.parentElement.className) && !isExcludedId(window, node.parentElement.id) && !isExcludedId(window, node.id));
otherNodes.push(
...cleanOptionTags,
)
}
// translate input type="submit" & input type="button"
const inputTypeButtonTags = Array.from(window.document.querySelectorAll('input[type="button"]'));
const inputTypeSubmitTags = Array.from(window.document.querySelectorAll('input[type="submit"]'));
const cleanInputTypeButtonTags = inputTypeButtonTags.filter((node) => (node.value || "").trim() && !isExcludedClassName(window,node.className) && !isExcludedId(window, node.id));
const cleanInputTypeSubmitTags = inputTypeSubmitTags.filter((node) => (node.value || "").trim() && !isExcludedClassName(window,node.className) && !isExcludedId(window, node.id));
otherNodes.push(
...cleanInputTypeButtonTags,
...cleanInputTypeSubmitTags,
)
const textNodes = [];
extractTextNodes(window, rootElement, textNodes);
const validTextNodes = filterValidTextNodes(textNodes) || [];
// console.log("validTextNodes", validTextNodes)
// handle a case where nodes already translated but some new texts are not translated yet
// for example on initial load in homepage: ['good morning'] -> ['guten morgen']
// then the user go to new route "/about", new dom added: ['guten morgen', 'good afternoon'] (this happen especially in nextjs because the route changes happens in client side)
// this will ensure only good afternoon is included
// list all cache values
if (!window.translationCache) {
window.translationCache = {};
}
const allLangCacheInAllPages = Object.keys(window.translationCache).reduce((prevValue, pathname) => {
const pageCache = window.translationCache[pathname]; // { en: {}, de: {}, id: {}}
Object.keys(pageCache).forEach(lang => {
if (!prevValue[lang]) {
prevValue[lang] = {};
}
const filteredPageCache = Object.keys(pageCache[lang])
.filter(key => !key.startsWith(MERGE_PREFIX))
.reduce((obj, key) => {
obj[key] = pageCache[lang][key];
return obj;
}, {});
prevValue[lang] = {...prevValue[lang], ...filteredPageCache};
});
return prevValue;
}, {});
const values = Object.values(allLangCacheInAllPages).flatMap(Object.values).filter(Boolean);
const textNodeThatNotInPrevPage = validTextNodes.reduce((prevTextNode, textNode) => {
// textContent as default
textNode.originalTextContent = textNode.textContent;
// merge
if (textNode.fullTextArray) {
// console.log("textNodeThatNotInPrevPage FULLTEXTARRAY", textNode.fullTextArray)
return [...prevTextNode, textNode]
}
const isInCache = values.includes(textNode.textContent);
// if the text is not in the cache, then add it to the list
if (!isInCache) {
// console.log("textNodeThatNotInPrevPage isInCache", isInCache)
return [...prevTextNode, textNode]
}
// if the text is already translated in the previous page, then find the original text
if (window.langHistory?.length) {
const existingCache = Object.values(allLangCacheInAllPages).reduce((prev, value) => [...prev, ...Object.entries(value)], [])
.find(([, value]) => {
return value == textNode.textContent
})
// original text found
if (existingCache) {
textNode.originalTextContent = existingCache[0];
if (textNode.context) {
textNode.context.replace(textNode.textContent, textNode.originalTextContent)
}
}
// }
}
return [...prevTextNode, textNode];
}, [])
translateNodes(window, textNodeThatNotInPrevPage, language, apiKey, seoNodes, otherNodes).then(() => {
setIsTranslationInitialized(window, true);
}).catch(reject).finally(() => {
window.globalseoTranslating = false;
renderSelectorState(window);
// console.log("GLOBALSEO: translateNodes");
resolve(undefined);
});
});
}
module.exports = modifyHtmlStrings;