Skip to content

Commit

Permalink
added translation
Browse files Browse the repository at this point in the history
  • Loading branch information
novcmbro committed Jun 6, 2024
1 parent 4faa8f0 commit f85e84c
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 47 deletions.
47 changes: 32 additions & 15 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Decoder</title>
<meta http-equiv="Content-Language" content="en">
<meta name="description" content="Encrypt and decrypt text easily with Decoder. Input your text, choose a method, and get the encrypted or decrypted result instantly!">
<meta name="description" content="Encrypt and decrypt text easily with Decoder. Type your text, choose a method, and get the result instantly!">
<meta name="keywords" content="decoder, encrypt, decrypt, online tool, text encoding, cryptography, Oracle Next Education, ONE, Alura">
<meta name="author" content="Novcmbro">
<meta name="robots" content="index, follow">
Expand All @@ -17,13 +17,13 @@
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:title" name="title" content="Decoder">
<meta property="og:description" name="description" content="Encrypt and decrypt text easily with Decoder. Input your text, choose a method, and get the encrypted or decrypted result instantly!">
<meta property="og:description" name="description" content="Encrypt and decrypt text easily with Decoder. Type your text, choose a method, and get the result instantly!">
<meta property="og:image" name="image" content="https://raw.githubusercontent.com/novcmbro/decoder/main/public/og-image.png">
<meta property="og:url" content="https://novcmbro.github.io/decoder">
<!-- Twitter / X -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:title" name="title" content="Decoder">
<meta property="twitter:description" name="description" content="Encrypt and decrypt text easily with Decoder. Input your text, choose a method, and get the encrypted or decrypted result instantly!">
<meta property="twitter:description" name="description" content="Encrypt and decrypt text easily with Decoder. Type your text, choose a method, and get the result instantly!">
<meta property="twitter:image" name="image" content="https://raw.githubusercontent.com/novcmbro/decoder/main/public/twitter-card.png">
<meta property="twitter:url" content="https://novcmbro.github.io/decoder">
<!-- Fonts -->
Expand All @@ -46,39 +46,56 @@
<header>
<img src="src/img/decoder-logo.png" alt="Decoder logo" class="logo" role="img">
</header>
<main>
<main class="container">
<section class="input-section" aria-labelledby="input-label">
<label id="input-label" for="input-field">Input</label>
<label id="input-label" for="input-field" data-translation="input.label">Input</label>
<textarea id="input-field" placeholder="Type your text" class="input-field" aria-describedby="input-field-message"></textarea>
<div class="input-field-message" role="alert" aria-live="assertive" aria-atomic="true">
<span id="alert-icon-label" hidden aria-hidden="true">Alert</span>
<span id="alert-icon-label" hidden aria-hidden="true" data-translation="input.message.icon">Alert</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="alert-icon" aria-labelledby="alert-icon-label">
<path d="M16 8C16 10.1217 15.1571 12.1566 13.6569 13.6569C12.1566 15.1571 10.1217 16 8 16C5.87827 16 3.84344 15.1571 2.34315 13.6569C0.842855 12.1566 0 10.1217 0 8C0 5.87827 0.842855 3.84344 2.34315 2.34315C3.84344 0.842855 5.87827 0 8 0C10.1217 0 12.1566 0.842855 13.6569 2.34315C15.1571 3.84344 16 5.87827 16 8ZM8 4C7.87361 4.00007 7.74863 4.02662 7.63312 4.07793C7.51761 4.12924 7.41413 4.20418 7.32934 4.29791C7.24456 4.39165 7.18035 4.5021 7.14084 4.62217C7.10134 4.74223 7.08743 4.86923 7.1 4.995L7.45 8.502C7.46176 8.63977 7.5248 8.76811 7.62664 8.86164C7.72849 8.95516 7.86173 9.00705 8 9.00705C8.13827 9.00705 8.27151 8.95516 8.37336 8.86164C8.4752 8.76811 8.53824 8.63977 8.55 8.502L8.9 4.995C8.91257 4.86923 8.89866 4.74223 8.85915 4.62217C8.81965 4.5021 8.75544 4.39165 8.67066 4.29791C8.58587 4.20418 8.48239 4.12924 8.36688 4.07793C8.25137 4.02662 8.12639 4.00007 8 4ZM8.002 10C7.73678 10 7.48243 10.1054 7.29489 10.2929C7.10736 10.4804 7.002 10.7348 7.002 11C7.002 11.2652 7.10736 11.5196 7.29489 11.7071C7.48243 11.8946 7.73678 12 8.002 12C8.26722 12 8.52157 11.8946 8.70911 11.7071C8.89664 11.5196 9.002 11.2652 9.002 11C9.002 10.7348 8.89664 10.4804 8.70911 10.2929C8.52157 10.1054 8.26722 10 8.002 10Z"></path>
</svg>
<p id="input-field-message">Only lowercase letters with no accent (a-z), dots (.), commas (,), exclamation and question marks (! ?) are accepted.</p>
<p id="input-field-message" data-translation="input.message.text">Only lowercase letters with no accent (a-z), dots (.), commas (,), exclamation (!) and question marks (?) are accepted.</p>
</div>
<div class="button-container">
<button id="encrypt-button" type="button" class="button button-primary">Encrypt</button>
<button id="decrypt-button" type="button" class="button button-secondary">Decrypt</button>
<button id="encrypt-button" type="button" class="button button-primary" data-translation="input.encrypt">Encrypt</button>
<button id="decrypt-button" type="button" class="button button-secondary" data-translation="input.decrypt">Decrypt</button>
</div>
</section>
<section class="output-section" aria-labelledby="output-label">
<div id="output-placeholder" class="output-placeholder">
<img src="src/img/output-placeholder-image.svg" alt="Placeholder" class="output-placeholder-image" role="img">
<h3 class="output-placeholder-title">No text found</h3>
<p>Type a text you wish to encrypt or decrypt</p>
<img id="output-placeholder-image" src="src/img/output-placeholder-image.svg" alt="Girl on computer" class="output-placeholder-image" role="img">
<h3 class="output-placeholder-title" data-translation="output.placeholder.title">No text found</h3>
<p data-translation="output.placeholder.description">Type a text you wish to encrypt or decrypt</p>
</div>
<div class="output-result">
<label id="output-label" for="output-field">Output</label>
<label id="output-label" for="output-field" data-translation="output.label">Output</label>
<textarea id="output-field" class="output-field" readonly tabindex="-1" aria-readonly="true" aria-disabled="true"></textarea>
<div class="button-container">
<button id="copy-button" type="button" class="button button-tertiary">Copy</button>
<button id="copy-button" type="button" class="button button-tertiary" data-translation="output.copy.text">Copy</button>
</div>
</div>
</section>
</main>
<footer>
made with 💜 by <a href="https://github.com/novcmbro" class="footer-novcmbro-link" target="_blank" rel="noopener noreferrer" aria-label="GitHub">Novcmbro</a>
<div class="container">
<nav class="footer-language-nav" aria-labelledby="language-nav-label">
<span id="language-nav-label" hidden aria-hidden="true" data-translation="footer.language.label">Change language</span>
<span id="language-icon-label" hidden aria-hidden="true" data-translation="footer.language.icon">Globe</span>
<svg width="24" height="24" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="language-icon" aria-labelledby="language-icon-label">
<g transform="translate(-204.000000, -671.000000)" fill="currentColor">
<path d="M231.596,694.829 C229.681,694.192 227.622,693.716 225.455,693.408 C225.75,691.675 225.907,689.859 225.957,688 L233.962,688 C233.783,690.521 232.936,692.854 231.596,694.829 L231.596,694.829 Z M223.434,700.559 C224.1,698.95 224.645,697.211 225.064,695.379 C226.862,695.645 228.586,696.038 230.219,696.554 C228.415,698.477 226.073,699.892 223.434,700.559 L223.434,700.559 Z M220.971,700.951 C220.649,700.974 220.328,701 220,701 C219.672,701 219.352,700.974 219.029,700.951 C218.178,699.179 217.489,697.207 216.979,695.114 C217.973,695.027 218.98,694.976 220,694.976 C221.02,694.976 222.027,695.027 223.022,695.114 C222.511,697.207 221.822,699.179 220.971,700.951 L220.971,700.951 Z M209.781,696.554 C211.414,696.038 213.138,695.645 214.936,695.379 C215.355,697.211 215.9,698.95 216.566,700.559 C213.927,699.892 211.586,698.477 209.781,696.554 L209.781,696.554 Z M208.404,694.829 C207.064,692.854 206.217,690.521 206.038,688 L214.043,688 C214.093,689.859 214.25,691.675 214.545,693.408 C212.378,693.716 210.319,694.192 208.404,694.829 L208.404,694.829 Z M208.404,679.171 C210.319,679.808 212.378,680.285 214.545,680.592 C214.25,682.325 214.093,684.141 214.043,686 L206.038,686 C206.217,683.479 207.064,681.146 208.404,679.171 L208.404,679.171 Z M216.566,673.441 C215.9,675.05 215.355,676.789 214.936,678.621 C213.138,678.356 211.414,677.962 209.781,677.446 C211.586,675.523 213.927,674.108 216.566,673.441 L216.566,673.441 Z M219.029,673.049 C219.352,673.027 219.672,673 220,673 C220.328,673 220.649,673.027 220.971,673.049 C221.822,674.821 222.511,676.794 223.022,678.886 C222.027,678.973 221.02,679.024 220,679.024 C218.98,679.024 217.973,678.973 216.979,678.886 C217.489,676.794 218.178,674.821 219.029,673.049 L219.029,673.049 Z M223.954,688 C223.9,689.761 223.74,691.493 223.439,693.156 C222.313,693.058 221.168,693 220,693 C218.832,693 217.687,693.058 216.562,693.156 C216.26,691.493 216.1,689.761 216.047,688 L223.954,688 L223.954,688 Z M216.047,686 C216.1,684.239 216.26,682.507 216.562,680.844 C217.687,680.942 218.832,681 220,681 C221.168,681 222.313,680.942 223.438,680.844 C223.74,682.507 223.9,684.239 223.954,686 L216.047,686 L216.047,686 Z M230.219,677.446 C228.586,677.962 226.862,678.356 225.064,678.621 C224.645,676.789 224.1,675.05 223.434,673.441 C226.073,674.108 228.415,675.523 230.219,677.446 L230.219,677.446 Z M231.596,679.171 C232.936,681.146 233.783,683.479 233.962,686 L225.957,686 C225.907,684.141 225.75,682.325 225.455,680.592 C227.622,680.285 229.681,679.808 231.596,679.171 L231.596,679.171 Z M220,671 C211.164,671 204,678.163 204,687 C204,695.837 211.164,703 220,703 C228.836,703 236,695.837 236,687 C236,678.163 228.836,671 220,671 L220,671 Z"></path>
</g>
</svg>
<span id="language-link-en-label" hidden aria-hidden="true">English</span>
<a id="language-link-en" href="#" class="footer-language-link en" aria-labelledby="language-link-en-label">EN<a>
<span id="language-link-pt-label" hidden aria-hidden="true">Português</span>
<a id="language-link-pt" href="#" class="footer-language-link pt" aria-labelledby="language-link-pt-label">PT</a>
</nav>
<p class="footer-credits">
<span data-translation="footer.credits">made with 💜 by</span> <a href="https://github.com/novcmbro" class="footer-novcmbro-link" target="_blank" rel="noopener noreferrer" aria-label="GitHub">Novcmbro</a>
</p>
</div>
</footer>
</body>
</html>
49 changes: 28 additions & 21 deletions src/css/footer.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
38 changes: 34 additions & 4 deletions src/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
box-sizing: border-box;
}

.container {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}

body {
display: flex;
flex-direction: column;
Expand All @@ -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;
Expand Down
8 changes: 5 additions & 3 deletions src/js/copyOutputText.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
21 changes: 17 additions & 4 deletions src/js/index.js
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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]))
Expand All @@ -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)) {
Expand Down Expand Up @@ -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()
})
})
60 changes: 60 additions & 0 deletions src/js/translation.js
Original file line number Diff line number Diff line change
@@ -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()
}
Loading

0 comments on commit f85e84c

Please sign in to comment.