UNPKG

weploy-translate

Version:

Translate your React.js or Next.js app with AI

491 lines (408 loc) 21.4 kB
const { isBrowser, BRAND } = require("./utils/configs"); const detectRobot = require("./utils/detectRobot"); const getPrefixedPathname = require("./utils/translation-mode/getPrefixedPathname"); const getUnprefixedPathname = require("./utils/translation-mode/getUnprefixedPathname"); function replaceCustomLinks(window, customLinks = {}, domainFromServer) { // find all tags with href and src const tagsWithSrc = window.document.querySelectorAll("[src]"); const tagsWithHref = window.document.querySelectorAll("[href]"); // replace the links for (let tag of tagsWithSrc) { const src = tag.getAttribute("src"); if (customLinks[src]) { tag.setAttribute("src", customLinks[src]); } else { const url = new URL(src, window.location.origin); const hostname = url.hostname; const hostnameWithoutWWW = hostname.replace("www.", ""); if (domainFromServer == hostnameWithoutWWW) { tag.setAttribute("src", url.href); } } } for (let tag of tagsWithHref) { const href = tag.getAttribute("href"); if (customLinks[href]) { tag.setAttribute("href", customLinks[href]); } else { const url = new URL(href, window.location.origin); const hostname = url.hostname; const hostnameWithoutWWW = hostname.replace("www.", ""); if (domainFromServer == hostnameWithoutWWW) { tag.setAttribute("href", url.href); } } } } /** * Extracts options from a script. * * @param {any} window - The global window object. * @param {Object} optsArgs - Options for extraction. * @param {string|null} optsArgs.activeLanguage - The active language (optional). * @param {string|null} optsArgs.domainFromServer - The domain from the server (optional). * @param {"globalseo"|"weploy"} [optsArgs.brand] - The brand name. * - "searchParams": Extract options from query parameters. * - "subdomain": Extract options from subdomains. * - "pathname": Extract options from the path. */ function extractOptionsFromScript(window, optsArgs = { activeLanguage: null, domainFromServer: null, }) { if (isBrowser()) { if (!window.translationCache) { window.translationCache = {}; } window.currentPathname = window.location.pathname } // REQUIRED ATTRIBUTES const DATA_API_KEY = `data-${optsArgs.brand || BRAND}-key` const DATA_ORIGINAL_LANG = "data-original-language"; const DATA_ALLOWED_LANGUAGES = "data-allowed-languages"; const apiKey = window.translationScriptTag.getAttribute(DATA_API_KEY); // COMMON OPTIONAL ATTRIBUTES const DATA_USE_BROWSER_LANG = "data-use-browser-language"; // default: true const DATA_EXCLUDE_CLASSES = "data-exclude-classes"; const DATA_EXCLUDE_IDS = "data-exclude-ids"; const DATA_EXCLUDE_PATHS = "data-exclude-paths"; const DATA_REPLACE_LINKS = "data-replace-links"; // default: true const DATA_AUTO_CREATE_SELECTOR = "data-auto-create-selector"; // default: true const DATA_DELAY = "data-timeout"; // default: 0 const DATA_DYNAMIC_TRANSLATION = "data-dynamic-translation"; // default: true const DATA_TRANSLATE_ATTR = "data-translate-attributes"; // default: false // SUBDOMAIN OPTIONAL ATTRIBUTES const DATA_TRANSLATION_MODE = "data-translation-mode"; // default: "searchParams" // ADVANCED OPTIONAL ATTRIBUTES const DATA_LANG_PARAMETER = "data-lang-parameter"; // default: "lang" const DATA_CUSTOM_LANG_CODE = "data-custom-language-code"; // format: "kk=kz, en=us, ru=rs" const DATA_MERGE_INLINE = "data-translate-splitted-text"; // default: false const DATA_EXCLUDE_CONTENTS = "data-exclude-contents"; // format: "{{content1}}, {{content2}}" const DATA_DEBOUNCE = "data-debounce" // format: "2000" const DATA_TRANSLATE_FORM_PLACEHOLDER = "data-translate-form-placeholder" const DATA_TRANSLATE_SELECT_OPTIONS = "data-translate-select-options" const DATA_DOMAIN_SOURCE_PREFIX = "data-domain-source-prefix" const DATA_CUSTOM_LINKS = "data-custom-links" const DATA_TRANSLATION_CACHE = "data-translation-cache"; const DATA_SOURCE_ORIGIN = "data-source-origin"; // WORKER ATTRIBUTES // const DATA_PREVENT_INIT_TRANSLATION = "data-prevent-init-translation" // default: false // const preventInitialTranslation = window.translationScriptTag.getAttribute(DATA_PREVENT_INIT_TRANSLATION) == "true"; const DATA_ACTIVE_SUBDOMAIN = "data-active-subdomain"; // default: undefined const activeSubdomain = window.translationScriptTag.getAttribute(DATA_ACTIVE_SUBDOMAIN); const DATA_ACTIVE_SUBDIRECTORY = "data-active-subdirectory"; // default: undefined const activeSubdirectory = window.translationScriptTag.getAttribute(DATA_ACTIVE_SUBDIRECTORY); // prevent initial translation for subdomain (but allow translation on dynamic content) if (activeSubdomain) { window.preventInitialTranslation = true; window.activeSubdomain = activeSubdomain; } if (activeSubdirectory) { window.preventInitialTranslation = true; window.activeSubdirectory = activeSubdirectory; } const activeServerSideLang = activeSubdomain || activeSubdirectory; const customLinksAttribute = window.translationScriptTag.getAttribute(`${DATA_CUSTOM_LINKS}-${activeServerSideLang}`); let customLinks = {}; try { // format: [oldUrl,newUrl], [oldUrl,newUrl] const parsed = JSON.parse(`[${customLinksAttribute.replaceAll('\'', '"')}]`); customLinks = parsed.reduce((acc, [oldUrl, newUrl]) => { acc[oldUrl] = newUrl; return acc; }, {}); replaceCustomLinks(window, customLinks, optsArgs.domainFromServer); } catch (e) { customLinks = {}; } // FEATURE: Prevent Google Translate from translating the page // Set the 'translate' attribute of the HTML tag to 'no' window.document.documentElement.setAttribute('translate', 'no'); // Create a new meta element const meta = window.document.createElement('meta'); meta.name = 'google'; meta.content = 'notranslate'; // Append the meta element to the head of the document window.document.head.appendChild(meta); // FEATURE: Get text inside brackets const excludeContentsRegex = /{{((?:[^}]|\\})*)}}/g; function getTextInsideBrackets(str) { let match; let matches = []; while ((match = excludeContentsRegex.exec(str)) !== null) { // Replace escaped closing brackets with a single closing bracket let cleanedMatch = match[1].replace(/\\\\}/g, '}'); cleanedMatch = cleanedMatch.replace(/\\\\{/g, '{'); matches.push(cleanedMatch); } return matches; } const translationMode = window.translationScriptTag.getAttribute(DATA_TRANSLATION_MODE) || "searchParams"; const langParam = window.translationScriptTag.getAttribute(DATA_LANG_PARAMETER) || "lang"; const search = window.location.search; const params = new URLSearchParams(search); const paramsLang = window.activeSubdomain || window.activeSubdirectory || optsArgs.activeLanguage || params.get(langParam); window.paramsLang = paramsLang; const paramsUpdateTranslation = params.get('globalseo_update_translation'); // defined languages so dont need extra fetch const originalLangAttr = window.translationScriptTag.getAttribute(DATA_ORIGINAL_LANG) || window.translationScriptTag.getAttribute("data-original-lanugage"); const originalLang = (originalLangAttr || "").trim().toLowerCase(); const allowedLangAttr = window.translationScriptTag.getAttribute(DATA_ALLOWED_LANGUAGES); const allowedLangs = (allowedLangAttr || "").trim().toLowerCase().split(",").filter(lang => lang && lang.trim() != originalLang).map(lang => lang.trim()); // always use original language for subdomain let activeLang = (window.globalseoActiveLang || paramsLang || originalLang); if (translationMode == "subdomain" && !window.isWorker) { activeLang = window.globalseoActiveLang } if (translationMode == "subdirectory" && !window.isWorker) { activeLang = window.globalseoActiveLang } if (activeLang && (window.document.documentElement.lang != activeLang)) { window.document.documentElement.lang = activeLang; } const domainSourcePrefixAttr = window.translationScriptTag.getAttribute(DATA_DOMAIN_SOURCE_PREFIX) || ""; // cleaned up the slashes at the beginning and end (clean up mutiple slashes too) const domainSourcePrefix = domainSourcePrefixAttr.replace(/^\/+|\/+$/g, ""); function handleLinkTags() { const domainWithoutWww = window.location.hostname.split('.').slice(1).join('.').replace("https://www.", "https://").replace("http://www.", "http://"); let domain = window.location.hostname; if (activeSubdomain) { domain = domainWithoutWww } if (activeSubdirectory && optsArgs.domainFromServer) { domain = optsArgs.domainFromServer; } // FEATURE: Create a canonical link tag for translated pages // e.g. https://example.com/path?lang=es // Cleanup the original canonical link tag const existingCanonicalLinkTag = window.document.querySelector('link[rel="canonical"]'); if (existingCanonicalLinkTag) { existingCanonicalLinkTag.remove(); } const newCanonicalLinkTag = window.document.createElement('link'); if (translationMode == "searchParams") { if (!paramsLang || paramsLang == originalLang) { newCanonicalLinkTag.href = `${window.location.protocol}//${window.location.host}${window.location.pathname}`; } else { newCanonicalLinkTag.href = `${window.location.protocol}//${window.location.host}${window.location.pathname}?${langParam}=${paramsLang}`; } } if (translationMode == "subdomain") { if (!activeLang || (activeLang == originalLang)) { newCanonicalLinkTag.href = `${window.location.protocol}//${domain}${window.location.pathname}`; } else { // Create a new URL object let url = new URL(window.location.href); url.hostname = `${activeLang}.${domain}`; url.pathname = getUnprefixedPathname(window, domainSourcePrefix, url.pathname); newCanonicalLinkTag.href = url.href; } } if (translationMode == "subdirectory") { if (!activeLang || (activeLang == originalLang)) { newCanonicalLinkTag.href = `${window.location.protocol}//${domain}${window.location.pathname}`; } else { let url = new URL(window.location.href); url.hostname = domain; // url.pathname = url.pathname.replace(domainSourcePrefix, ""); url.pathname = getUnprefixedPathname(window, domainSourcePrefix, url.pathname); newCanonicalLinkTag.href = url.href; } } newCanonicalLinkTag.setAttribute('rel', 'canonical'); window.document.head.appendChild(newCanonicalLinkTag); // Add alternate link tag for original languages const alternateLinkTag = window.document.createElement('link'); alternateLinkTag.setAttribute('rel', 'alternate'); alternateLinkTag.setAttribute('hreflang', originalLang); const subdirUrl = new URL(window.location.href); // cleanup from prefix and language to get the real pathname subdirUrl.pathname = getUnprefixedPathname(window, domainSourcePrefix, subdirUrl.pathname); subdirUrl.pathname = subdirUrl.pathname.replace(`/${activeSubdirectory}/`, '/'); const cleanPathname = subdirUrl.pathname; // const cleanPathname = `${window.location.pathname.replace(domainSourcePrefix, "").replace(`/${originalLang}/`, '/')}`; // cleanup from language to get the real pathname const originalPathname = getPrefixedPathname(window, domainSourcePrefix, subdirUrl.pathname) // add back the prefix const subdirectoryHref = `${window.location.protocol}//${domain}${originalPathname}` // append prefix to the original lang because the subdomain will be accessed without the prefix (dont append if subdirectory mode) alternateLinkTag.href = translationMode == "subdirectory" ? subdirectoryHref : `${window.location.protocol}//${domain}${getPrefixedPathname(window, domainSourcePrefix, window.location.pathname)}`; window.document.head.appendChild(alternateLinkTag); // Add alternate link tags for allowed languages for (let lang of allowedLangs) { const alternateLinkTag = window.document.createElement('link'); // Create a new URL object let url = new URL(window.location.href); // Set the search parameter "lang" to lang url.searchParams.set(langParam, lang); alternateLinkTag.setAttribute('rel', 'alternate'); alternateLinkTag.setAttribute('hreflang', lang); if (translationMode == "searchParams") { alternateLinkTag.href = `${window.location.protocol}//${window.location.host}${window.location.pathname}?${langParam}=${lang}`; } if (translationMode == "subdomain") { // Create a new URL object let url = new URL(window.location.href); url.hostname = activeSubdomain ? `${lang}.${domain}` : `${lang}.${domainWithoutWww}`; url.pathname = getUnprefixedPathname(window, domainSourcePrefix, url.pathname); alternateLinkTag.href = url.href; } if (translationMode == "subdirectory") { // remove lang param url.searchParams.delete(langParam); // append the first slash with lang // google.com -> google.com/en // url.pathname = url.pathname.replace(domainSourcePrefix, "") url.pathname = getUnprefixedPathname(window, domainSourcePrefix, url.pathname); url.pathname = url.pathname.replace(`/${activeSubdirectory}/`, '/'); // cleanup from prefix and language to get the real pathname let pathnames = url.pathname.split('/'); if (lang && (lang != originalLang)) pathnames.splice(1, 0, lang); url.pathname = pathnames.join('/'); if (lang == originalLang) url.pathname = `${domainSourcePrefix}${url.pathname}` alternateLinkTag.href = url.href; } window.document.head.appendChild(alternateLinkTag); } } if ((!activeSubdomain && !activeSubdirectory )|| window.isWorker) { handleLinkTags(); } // console.log(process.env.NO_CACHE) // // check if the page works without the trailing slash // checkPage(); if (isBrowser()) { // subdomain may dont need to check expiration if (!window.isWorker && !window.activeSubdomain && !window.activeSubdirectory) { try { // get the current date const now = new Date(); // get the current date timestamp const nowTimestamp = now.getTime(); // get existing expiration timestamp const globalseoExpirationTimestamp = window.localStorage.getItem("globalseoExpirationTimestamp"); const expiration = Number(globalseoExpirationTimestamp); if (!isNaN(expiration) && expiration < nowTimestamp) { window.localStorage.removeItem("globalseoExpirationTimestamp"); window.localStorage.removeItem("translationCachePerPage"); } } catch (e) { console.log("Error checking expiration", e) } } const userAgent = window.navigator.userAgent; const isRobot = detectRobot(userAgent); if (!isRobot) { const translationCache = window.localStorage.getItem("translationCachePerPage"); try { const parsedTranslationCache = JSON.parse(translationCache); if (parsedTranslationCache && typeof parsedTranslationCache === "object") { window.translationCache = parsedTranslationCache; } const injectedCacheElement = document.getElementById("globalseo-translation-cache"); if (injectedCacheElement) { try { const stringifiedCache = injectedCacheElement.textContent; const parsedCache = JSON.parse(stringifiedCache); window.translationCache = { ...window.translationCache, [window.location.pathname]: { ...window.translationCache[window.location.pathname], ...parsedCache[window.location.pathname] } } } catch(error) { // do nothing } } // handle cache coming from server side rendering const thisScriptTranslationCache = window.translationScriptTag.getAttribute(DATA_TRANSLATION_CACHE); if (thisScriptTranslationCache) { const parsedThisScriptTranslationCache = JSON.parse(thisScriptTranslationCache); window.translationCache = { ...window.translationCache, [window.location.pathname]: { ...window.translationCache[window.location.pathname], ...parsedThisScriptTranslationCache[window.location.pathname] } } } window.localStorage.setItem("translationCachePerPage", JSON.stringify(window.translationCache)); } catch (e) { console.log("Error parsing translation cache", e) } } else { window.translationCache = {}; } } const customLanguageCodeAttr = window.translationScriptTag.getAttribute(DATA_CUSTOM_LANG_CODE); // format is "kk=kz, en=us, ru=rs" const customLanguageCode = customLanguageCodeAttr ? customLanguageCodeAttr.split(",").reduce((acc, lang) => { const [key, value] = lang.trim().split("="); if (!key || !value) return acc; acc[key] = value; return acc; }, {}) : {}; // this is backward compatibility for use browser language const disableAutoTranslateAttr = window.translationScriptTag.getAttribute("data-disable-auto-translate"); const disableAutoTranslate = disableAutoTranslateAttr == "true"; const useBrowserLanguageAttr = window.translationScriptTag.getAttribute(DATA_USE_BROWSER_LANG); const useBrowserLanguage = useBrowserLanguageAttr != "false" && useBrowserLanguageAttr != false; // exclude classes const excludeClassesAttr = (window.translationScriptTag.getAttribute(DATA_EXCLUDE_CLASSES) || "").trim() const excludeClassesByComma = excludeClassesAttr.split(",").filter(className => !!className).map(className => className.trim()); const excludeClassesBySpace = excludeClassesAttr.split(" ").filter(className => !!className).map(className => className.trim().replaceAll(",", "")); const mergedExcludeClasses = [...excludeClassesByComma, ...excludeClassesBySpace]; const excludeClasses = [...new Set(mergedExcludeClasses)]; // get unique values // exclude ids const excludeIdsAttr = (window.translationScriptTag.getAttribute(DATA_EXCLUDE_IDS) || "").trim() const excludeIdsByComma = excludeIdsAttr.split(",").filter(id => !!id).map(id => id.trim()); const excludeIdsBySpace = excludeIdsAttr.split(" ").filter(id => !!id).map(id => id.trim().replaceAll(",", "")); const mergedExcludeIds = [...excludeIdsByComma, ...excludeIdsBySpace]; const excludeIds = [...new Set(mergedExcludeIds)]; // get unique values // exclude paths const excludePathsAttr = (window.translationScriptTag.getAttribute(DATA_EXCLUDE_PATHS) || "").trim() const excludePathsByComma = excludePathsAttr.split(",").filter(path => !!path).map(path => path.trim()); const excludePathsBySpace = excludePathsAttr.split(" ").filter(path => !!path).map(path => path.trim().replaceAll(",", "")); const mergedExcludePaths = [...excludePathsByComma, ...excludePathsBySpace]; const excludePaths = [...new Set(mergedExcludePaths)]; // get unique values // exclude contents const excludeContentsAttr = (window.translationScriptTag.getAttribute(DATA_EXCLUDE_CONTENTS) || "").trim() const splittedContents = getTextInsideBrackets(excludeContentsAttr); const excludeContents = [...new Set(splittedContents)]; // get unique values // timeout const timeoutAttr = window.translationScriptTag.getAttribute(DATA_DELAY); const timeout = isNaN(Number(timeoutAttr)) ? 0 : Number(timeoutAttr); const createSelector = window.translationScriptTag.getAttribute(DATA_AUTO_CREATE_SELECTOR) != "false"; const translateAttributes = window.translationScriptTag.getAttribute(DATA_TRANSLATE_ATTR) == "true"; const dynamicTranslation = paramsUpdateTranslation == "true" || (window.translationScriptTag.getAttribute(DATA_DYNAMIC_TRANSLATION) != "false"); const translateSplittedText = window.translationScriptTag.getAttribute(DATA_MERGE_INLINE) == "true"; const shouldReplaceLinks = window.translationScriptTag.getAttribute(DATA_REPLACE_LINKS) != "false"; const debounceDuration = window.translationScriptTag.getAttribute(DATA_DEBOUNCE); const translateFormPlaceholder = window.translationScriptTag.getAttribute(DATA_TRANSLATE_FORM_PLACEHOLDER) == "true"; const translateSelectOptions = window.translationScriptTag.getAttribute(DATA_TRANSLATE_SELECT_OPTIONS) == "true"; const sourceOrigin = window.translationScriptTag.getAttribute(DATA_SOURCE_ORIGIN); return { useBrowserLanguage: !disableAutoTranslate && useBrowserLanguage, createSelector: createSelector, excludeClasses, excludeIds, excludePaths, excludeContents, originalLanguage: originalLang, allowedLanguages: allowedLangs, timeout: timeout, customLanguageCode, translateAttributes, dynamicTranslation, translateSplittedText: false, langParam, debounceDuration, translateFormPlaceholder, translateSelectOptions, shouldReplaceLinks, paramsLang, apiKey, translationMode, domainSourcePrefix, customLinks, sourceOrigin, } } module.exports = extractOptionsFromScript;