diff --git a/index.html b/index.html index 5cadba3..748c392 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ Decoder - + @@ -17,13 +17,13 @@ - + - + @@ -46,39 +46,56 @@
-
+
- +
- - + +
- Placeholder -

No text found

-

Type a text you wish to encrypt or decrypt

+ Girl on computer +

No text found

+

Type a text you wish to encrypt or decrypt

- +
- +
diff --git a/src/css/footer.css b/src/css/footer.css index 76f90a5..dcea19d 100644 --- a/src/css/footer.css +++ b/src/css/footer.css @@ -1,32 +1,39 @@ -footer { +footer .container { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-sm) var(--spacing-base); text-align: center; padding: var(--spacing-base); } -.footer-novcmbro-link { - position: relative; - color: currentColor; - text-decoration: none; - font-weight: var(--font-bold); +.footer-language-nav .language-icon { + vertical-align: text-bottom; + margin-right: var(--spacing-sm); } -.footer-novcmbro-link:focus { - border: none; - outline: none; +.footer-language-link.pt:lang(en), +.footer-language-link.en:lang(pt) { + opacity: 0.47; } -.footer-novcmbro-link::after { - content: ""; - background: linear-gradient(to right, var(--gradient)); - position: absolute; - top: 100%; - left: 0; - width: 0; - height: var(--border-size); +.footer-language-link.pt:lang(en):hover, +.footer-language-link.en:lang(pt):hover { + opacity: 1; + transition: opacity var(--transition-duration); } -.footer-novcmbro-link:hover::after, -.footer-novcmbro-link:focus::after { - width: 100%; - transition: width var(--transition-duration); +.footer-novcmbro-link { + font-weight: var(--font-bold); +} + +@media screen and (min-width: 700px) { + footer .container { + flex-direction: row; + justify-content: space-between; + } + + .footer-credits { + margin: 0 auto; + } } diff --git a/src/css/index.css b/src/css/index.css index ef85ef2..f67cbff 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -46,6 +46,12 @@ box-sizing: border-box; } +.container { + width: 100%; + max-width: 1000px; + margin: 0 auto; +} + body { display: flex; flex-direction: column; @@ -57,13 +63,37 @@ main { flex-direction: column; gap: var(--spacing-base); flex-grow: 1; - width: 100%; - max-width: 1000px; - margin: 0 auto; - padding: var(--spacing-md); + padding: var(--spacing-base); padding-top: 0; } +a { + position: relative; + color: currentColor; + text-decoration: none; +} + +a:focus { + border: none; + outline: none; +} + +a::after { + content: ""; + background: linear-gradient(to right, var(--gradient)); + position: absolute; + top: 100%; + left: 0; + width: 0; + height: var(--border-size); +} + +a:hover::after, +a:focus::after { + width: 100%; + transition: width var(--transition-duration); +} + label { position: absolute; top: 0; diff --git a/src/js/copyOutputText.js b/src/js/copyOutputText.js index cd5260b..bc72d6d 100644 --- a/src/js/copyOutputText.js +++ b/src/js/copyOutputText.js @@ -1,8 +1,10 @@ +import { translation } from "./translation.js" + const copyOutputText = (outputField) => { const message = { - success: "Text copied to clipboard!", - error: "Something went wrong while copying. Please, try again.", - unsupported: "Unfortunately, copy functionality is not available on your browser." + success: translation("output.copy.success"), + error: translation("output.copy.error"), + unsupported: translation("output.copy.unsupported") } if (navigator.clipboard) { diff --git a/src/js/index.js b/src/js/index.js index 54f6c2c..b2bb874 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,7 +1,10 @@ +import { changeLanguage, initTranslation, translation } from "./translation.js" import cryptographyEntries from "./cryptographyEntries.js" import copyOutputText from "./copyOutputText.js" document.addEventListener("DOMContentLoaded", () => { + initTranslation() + const inputField = document.querySelector("#input-field") const inputSectionClasses = inputField.parentElement.classList const inputFieldMessage = document.querySelector("#input-field-message") @@ -11,6 +14,8 @@ document.addEventListener("DOMContentLoaded", () => { const encryptButton = document.querySelector("#encrypt-button") const decryptButton = document.querySelector("#decrypt-button") const copyButton = document.querySelector("#copy-button") + const languageLinkEN = document.querySelector("#language-link-en") + const languageLinkPT = document.querySelector("#language-link-pt") const cryptography = (target) => { const entries = target === encryptButton ? cryptographyEntries : Object.fromEntries(Object.entries(cryptographyEntries).map(([letter, word]) => [word, letter])) @@ -22,10 +27,10 @@ document.addEventListener("DOMContentLoaded", () => { const validateField = (target) => { const validations = { - empty: () => inputField.value.trim() || "Text cannot be empty.", - invalidChars: () => inputField.value.trim() && inputField.value.match(/^[a-z.,!?\s]+$/) || "Invalid text. Only lowercase letters with no accent (a-z), dots (.), commas (,), exclamation and question marks (! ?) are accepted.", - noCompatibleChars: () => inputField.value.match(cryptography(target).keys) || (target === encryptButton ? "Failed to encrypt. Text does not have enough compatible letters." : "Failed to decrypt. Text does not have enough compatible letters."), - alreadyDecoded: () => outputField.value !== cryptography(target).result || (target === encryptButton ? "Text is already encrypted." : "Text is already decrypted.") + empty: () => inputField.value.trim() || translation("input.validations.empty"), + invalidChars: () => inputField.value.trim() && inputField.value.match(/^[a-z.,!?\s]+$/) || translation("input.validations.invalid_chars"), + noCompatibleChars: () => inputField.value.match(cryptography(target).keys) || (target === encryptButton ? translation("input.validations.no_compatible_chars.encrypt") : translation("input.validations.no_compatible_chars.decrypt")), + alreadyDecoded: () => outputField.value !== cryptography(target).result || (target === encryptButton ? translation("input.validations.already_decoded.encrypt") : translation("input.validations.already_decoded.decrypt")) } for (const [name, validation] of Object.entries(validations)) { @@ -71,4 +76,12 @@ document.addEventListener("DOMContentLoaded", () => { encryptButton.addEventListener("click", handleCryptography) decryptButton.addEventListener("click", handleCryptography) copyButton.addEventListener("click", () => copyOutputText(outputField)) + languageLinkEN.addEventListener("click", (e) => { + changeLanguage(e) + clearInputFieldError() + }) + languageLinkPT.addEventListener("click", (e) => { + changeLanguage(e) + clearInputFieldError() + }) }) diff --git a/src/js/translation.js b/src/js/translation.js new file mode 100644 index 0000000..0d51ce5 --- /dev/null +++ b/src/js/translation.js @@ -0,0 +1,60 @@ +import enUS from "../locales/en-us.js" +import ptBR from "../locales/pt-br.js" + +const languages = { en: enUS, pt: ptBR } +const localStorageKey = "novcmbro_decoder_language" +const separator = { nesting: ".", word: "_" } + +export const translation = (key) => { + const nestedId = key.split(separator.nesting) + const language = localStorage.getItem(localStorageKey) + let text = languages[language] + + for (let i = 0; i < nestedId.length; i++) { + const key = nestedId[i] + text = text && text[key] + } + + return text +} + +const translateElements = () => { + document.querySelectorAll("meta[name='description']").forEach(description => description.content = translation("description")) + document.querySelector("meta[name='keywords']").content = translation("keywords") + document.querySelector("#input-field").placeholder = translation("input.placeholder") + document.querySelector("#output-placeholder-image").alt = translation("output.placeholder.image") +} + +export const initTranslation = () => { + const navigatorLanguage = navigator.language.split("-")[0].toLowerCase() + const navigatorOrFallbackLanguage = (navigatorLanguage === "en" || navigatorLanguage === "pt") ? navigatorLanguage : "en" + const language = localStorage.getItem(localStorageKey) + const elements = document.querySelectorAll("[data-translation]") + + if (!language) { + localStorage.setItem(localStorageKey, navigatorOrFallbackLanguage) + window.location.href = "/" + } + + document.documentElement.lang = language + document.querySelector("meta[http-equiv='Content-Language']").content = language + + for (const element of elements) { + const id = element.dataset.translation + const hasId = !!id.trim() + const hasInvalidId = hasId && !id.match(`^[a-z${separator.nesting}${separator.word}]+$`) + + if (hasId && !hasInvalidId) { + element.ariaLive = "polite" + element.textContent = translation(id) + } + } + + translateElements() +} + +export const changeLanguage = ({ target }) => { + localStorage.setItem(localStorageKey, target.textContent.toLowerCase()) + initTranslation() + translateElements() +} diff --git a/src/locales/en-us.js b/src/locales/en-us.js new file mode 100644 index 0000000..43fcdd7 --- /dev/null +++ b/src/locales/en-us.js @@ -0,0 +1,49 @@ +const enUS = { + description: "Encrypt and decrypt text easily with Decoder. Type your text, choose a method, and get the result instantly!", + keywords: "decoder, encrypt, decrypt, online tool, text encoding, cryptography, Oracle Next Education, ONE, Alura", + input: { + label: "Input", + placeholder: "Type your text", + message: { + icon: "Alert", + text: "Only lowercase letters with no accent (a-z), dots (.), commas (,), exclamation (!) and question marks (?) are accepted." + }, + encrypt: "Encrypt", + decrypt: "Decrypt", + validations: { + empty: "Text cannot be empty.", + invalid_chars: "Invalid text. Only lowercase letters with no accent (a-z), dots (.), commas (,), exclamation (!) and question marks (?) are accepted.", + no_compatible_chars: { + encrypt: "Failed to encrypt. Text does not have enough compatible letters.", + decrypt: "Failed to decrypt. Text does not have enough compatible letters." + }, + already_decoded: { + encrypt: "Text is already encrypted.", + decrypt: "Text is already decrypted." + } + } + }, + output: { + label: "Output", + placeholder: { + image: "Girl on computer", + title: "No text found", + description: "Type a text you wish to encrypt or decrypt" + }, + copy: { + text: "Copy", + success: "Text copied to clipboard!", + error: "Something went wrong while copying. Please, try again.", + unsupported: "Unfortunately, copy functionality is not available on your browser." + } + }, + footer: { + language: { + label: "Change language", + icon: "Globe" + }, + credits: "made with 💜 by" + } +} + +export default enUS diff --git a/src/locales/pt-br.js b/src/locales/pt-br.js new file mode 100644 index 0000000..f78b2d5 --- /dev/null +++ b/src/locales/pt-br.js @@ -0,0 +1,49 @@ +const ptBR = { + description: "Criptografar e descriptografar texto facilmente com Decoder. Digite seu texto, escolha um método, e tenha o resultado instantaneamente!", + keywords: "decoder, criptografar, descriptografar, ferramenta online, codificação de texto, criptografia, Oracle Next Education, ONE, Alura", + input: { + label: "Entrada", + placeholder: "Digite seu texto", + message: { + icon: "Alerta", + text: "Apenas letras minúsculas sem acento (a-z), pontos (.), vírgulas (,), pontos de exclamação (!) e interrogação (?) são aceitos." + }, + encrypt: "Criptografar", + decrypt: "Descriptografar", + validations: { + empty: "Texto não pode estar vazio.", + invalid_chars: "Texto inválido. Apenas letras minúsculas sem acento (a-z), pontos (.), vírgulas (,), pontos de exclamação (!) e interrogação (?) são aceitos.", + no_compatible_chars: { + encrypt: "Falha ao criptografar. Texto não possui letras compatíveis suficientes.", + decrypt: "Falha ao descriptografar. Texto não possui letras compatíveis suficientes." + }, + already_decoded: { + encrypt: "Texto já está criptografado.", + decrypt: "Texto já está descriptografado." + } + } + }, + output: { + label: "Saída", + placeholder: { + image: "Garota no computador", + title: "Nenhum texto encontrado", + description: "Digite um texto que você deseja criptografar ou descriptografar" + }, + copy: { + text: "Copiar", + success: "Texto copiado para a área de transferência!", + error: "Algo deu errado durante a cópia. Por favor, tente novamente.", + unsupported: "Infelizmente, a funcionalidade de cópia não está disponível no seu navegador." + } + }, + footer: { + language: { + label: "Mudar idioma", + icon: "Globo" + }, + credits: "feito com 💜 por" + } +} + +export default ptBR