Merge pull request 'Add i18n support with portuguese language' (#5) from setup_i18n into development

Reviewed-on: #5
This commit is contained in:
Leonardo Murça 2025-03-23 18:09:49 +00:00
commit 9bf7937a65
12 changed files with 233 additions and 32 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "embroidery-viewer",
"version": "1.2.1",
"version": "1.2.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "embroidery-viewer",
"version": "1.2.1",
"version": "1.2.4",
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.1",
"svelte": "^3.59.1",

View file

@ -1,7 +1,7 @@
{
"name": "embroidery-viewer",
"private": true,
"version": "1.2.2",
"version": "1.2.4",
"type": "module",
"scripts": {
"dev": "vite",

View file

@ -47,4 +47,15 @@ input[type="submit"]:hover {
cursor: pointer;
background-color: black;
color: white;
}
}
body a {
text-decoration: none;
color: #06345F;
border-bottom: 3px solid #06345F;
}
body a:hover {
background-color: #06345F;
color: #ffffff;
}

View file

@ -2,7 +2,7 @@ import { jDataView } from "./jdataview";
import { supportedFormats } from "../format-readers";
import { Pattern } from "./pattern";
function renderFile(filename, evt, canvas, colorView, stitchesView, sizeView) {
function renderFile(filename, evt, canvas, colorView, stitchesView, sizeView, localizedStrings) {
const fileExtension = filename.toLowerCase().split(".").pop();
const view = jDataView(evt.target.result, 0, evt.size);
const pattern = new Pattern();
@ -12,8 +12,8 @@ function renderFile(filename, evt, canvas, colorView, stitchesView, sizeView) {
pattern.moveToPositive();
pattern.drawShapeTo(canvas);
pattern.drawColorsTo(colorView);
pattern.drawStitchesCountTo(stitchesView);
pattern.drawSizeValuesTo(stitchesView);
pattern.drawStitchesCountTo(stitchesView, localizedStrings.stitches);
pattern.drawSizeValuesTo(stitchesView, localizedStrings.dimensions);
}
function renderAbortMessage(errorMessageRef) {
@ -56,12 +56,13 @@ export default function renderFileToCanvas(
errorMessageRef,
colorView,
stitchesView,
sizeView
sizeView,
localizedStrings
) {
const reader = new FileReader();
reader.onloadend = (evt) =>
renderFile(fileObject.name, evt, canvas, colorView, stitchesView, sizeView);
renderFile(fileObject.name, evt, canvas, colorView, stitchesView, sizeView, localizedStrings);
reader.abort = (/** @type {any} */ _) => renderAbortMessage(errorMessageRef);
reader.onerror = (evt) =>
renderErrorMessage(evt.target.error.name, errorMessageRef);

View file

@ -212,12 +212,12 @@ Pattern.prototype.drawColorsTo = function (colorContainer) {
});
};
Pattern.prototype.drawStitchesCountTo = function (stitchesContainer) {
stitchesContainer.innerHTML += `<div><strong>Stitches:</strong> ${this.stitches.length} </div>`;
Pattern.prototype.drawStitchesCountTo = function (stitchesContainer, stitchesString) {
stitchesContainer.innerHTML += `<div><strong>${stitchesString}:</strong> ${this.stitches.length} </div>`;
};
Pattern.prototype.drawSizeValuesTo = function (sizeContainer) {
sizeContainer.innerHTML += `<div><strong>Size (x, y):</strong> ${Math.round(
Pattern.prototype.drawSizeValuesTo = function (sizeContainer, dimensionsString) {
sizeContainer.innerHTML += `<div><strong>${dimensionsString}:</strong> ${Math.round(
this.right / 10
)}mm x ${Math.round(this.bottom / 10)}mm </div>`;
};

35
src/i18n/index.js Normal file
View file

@ -0,0 +1,35 @@
import { derived, writable } from "svelte/store";
import translations from "./translations";
const browserLocale = navigator.language || "en";
const [baseLang, region] = browserLocale.split("-");
export const DEFAULT_LOCALE = translations[browserLocale] ? browserLocale : translations[baseLang] ? baseLang :"en";
export const locale = writable(DEFAULT_LOCALE);
export const locales = Object.entries(translations).map(([key, lang]) => [key, lang.name]);
function translate(locale, key, vars = {}) {
if (!key) throw new Error("Translation key is required.");
const validLocale = translations[locale]
? locale
: translations[baseLang]
? baseLang
: "en";
let text = translations[validLocale][key] || translations["en"][key];
if (!text) {
console.error(`Missing translation for key "${key}" in locale "${validLocale}".`);
return key;
}
return Object.entries(vars).reduce(
(str, [varKey, value]) => str.replaceAll(`{{${varKey}}}`, value),
text
);
}
export const t = derived(locale, ($locale) => (key, vars = {}) =>
translate($locale, key, vars)
);

32
src/i18n/translations.js Normal file
View file

@ -0,0 +1,32 @@
export default {
en: {
"main.title": "Upload files",
"main.languageSwitch": "Mudar para Português",
"main.fileSize": "Max file size is <strong>{{fileSize}}kb</strong>.",
"main.supportedFormats": "Accepted formats: <strong>{{supportedFormats}}</strong>.",
"main.render": "Render files",
"main.dropzone": "Drag and drop files here or click to upload.",
"main.selected": "Selected files",
"main.rejected": "Rejected files",
"main.stitches": "Stitches",
"main.dimensions": "Dimensions (x, y)",
"main.download": "Download image",
"main.copyright": "Copyright © {{year}} <a href=\"{{website}}\" target=\"_blank\" rel=\"noreferrer\">Leonardo Murça</a>. <br/> All rights reserved.",
"main.version": "Version: {{version}}"
},
pt: {
"main.title": "Carregar arquivos",
"main.languageSwitch": "Switch to English",
"main.fileSize": "O tamanho máximo do arquivo é <strong>{{fileSize}}kb</strong>.",
"main.supportedFormats": "Formatos aceitos: <strong>{{supportedFormats}}</strong>.",
"main.render": "Renderizar arquivos",
"main.dropzone": "Arraste e solte os arquivos aqui ou clique para fazer upload.",
"main.selected": "Arquivos selecionados",
"main.rejected": "Arquivos recusados",
"main.stitches": "Pontos",
"main.dimensions": "Dimensões (x, y)",
"main.download": "Baixar imagem",
"main.copyright": "Copyright © {{year}} <a href=\"{{website}}/pt-br\" target=\"_blank\" rel=\"noreferrer\">Leonardo Murça</a>. <br/> Todos os direitos reservados.",
"main.version": "Versão: {{version}}"
},
};

View file

@ -1,4 +1,5 @@
<script>
import { t } from "../i18n"
import renderFileToCanvas from "../file-renderer";
export let files = [];
@ -7,6 +8,10 @@
let stitchesRefs = [];
let sizeRefs = [];
let errorMessageRef;
let localizedStrings = {
stitches: $t("main.stitches"),
dimensions: $t("main.dimensions"),
}
const downloadCanvasAsImage = (canvas, filename) => {
const image = canvas
@ -41,7 +46,7 @@
on:keydown={onKeydown}
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
>
Download
{$t("main.download")}
</div>
</div>
{canvasRefs[i] &&
@ -51,7 +56,8 @@
errorMessageRef,
colorRefs[i],
stitchesRefs[i],
sizeRefs[i]
sizeRefs[i],
localizedStrings
)}
{/each}
<!-- svelte-ignore a11y-missing-content -->

View file

@ -1,4 +1,6 @@
<script>
import { t } from "../i18n"
export let files;
export let supportedFormats;
export let onKeydown;
@ -17,7 +19,7 @@
on:drop|preventDefault|stopPropagation={onDrop}
>
<label id="file-label" for="file-input"
>Drag and drop files here or click to upload.</label
>{$t("main.dropzone")}</label
>
<input
id="file-input"

View file

@ -2,9 +2,11 @@
import CardList from "./CardList.svelte";
import Dropzone from "./Dropzone.svelte";
import FileList from "./FileList.svelte";
import LanguageIcon from './LanguageIcon.svelte';
import { filterFiles } from "../utils/filterFiles";
import { supportedFormats } from "../format-readers";
import { t, locale, locales } from "../i18n"
let acceptedFiles;
let rejectedFiles;
@ -44,6 +46,12 @@
document.getElementById("file-input").click();
}
};
const onSwitchToOppositeLang = () => {
const oppositeLang = locales.find(item => item[0] !== $locale);
locale.set(oppositeLang[0]);
}
</script>
<form
@ -51,11 +59,20 @@
enctype="multipart/form-data"
on:submit|preventDefault|stopPropagation={onSubmit}
>
<h2>Upload files</h2>
<div class="title-container">
<h2>{$t("main.title")}</h2>
<a class="common-switch {$locale === 'en' ? 'portuguese-switch' : 'english-switch' }" href="#" on:click|preventDefault={onSwitchToOppositeLang}>
<div style="display: flex; width: fit-content;">
<div class="language-icon">
<LanguageIcon cssClass="{$locale === 'en' ? 'portuguese-switch' : 'english-switch' }" />
</div>
<span>{$t("main.languageSwitch")}</span>
</div>
</a>
</div>
<p>
Max file size is <strong>{fileRequirements.maxSize / 1000}kb</strong>.
Accepted formats:
<strong>{fileRequirements.supportedFormats.join(", ")}</strong>.
{@html $t("main.fileSize", { fileSize: fileRequirements.maxSize / 1000 })}
{@html $t("main.supportedFormats", { supportedFormats: fileRequirements.supportedFormats.join(", ") })}
</p>
<Dropzone
@ -67,17 +84,57 @@
{onChange}
/>
<input type="submit" value="Render files" />
<input type="submit" value={$t("main.render")} />
</form>
{#if areAcceptedFilesRendered}
<CardList files={acceptedFiles} />
{:else}
<FileList title="Rejected Files" files={rejectedFiles} isError />
<FileList title="Selected Files" files={acceptedFiles} />
<FileList title={$t("main.selected")} files={acceptedFiles} />
<FileList title={$t("main.rejected")} files={rejectedFiles} isError />
{/if}
<style>
.title-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.language-icon {
width: 30px;
height: 24px;
}
.common-switch {
width: fit-content;
}
.portuguese-switch {
color: #0C8F27;
border-bottom: 3px solid #0C8F27;
fill: #0C8F27 !important;
}
.portuguese-switch:hover {
background: #0C8F27;
color: #ffffff;
fill: #ffffff !important;
}
.english-switch{
color: #BE0A2F;
border-bottom: 3px solid #BE0A2F;
width: fit-content;
fill: #BE0A2F !important;
}
.english-switch:hover {
background: #BE0A2F;
color: #ffffff;
fill: #ffffff !important;
}
@media only screen and (max-device-width: 812px) {
#form {
width: 100%;

View file

@ -1,17 +1,11 @@
<script>
import { t } from "../i18n"
import { appVersion } from "../utils/env";
</script>
<footer>
<p>
Copyright © {new Date().getFullYear()}
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
>Leonardo Murça</a
>.
</p>
<p>
version: {appVersion()}
</p>
<p>{@html $t("main.copyright", { year: new Date().getFullYear(), website: "https://leomurca.xyz" })}</p>
<p>{@html $t("main.version", { version: appVersion() })}</p>
</footer>
<style>

View file

@ -0,0 +1,63 @@
<script>
export let cssClass;
</script>
<svg version="1.1" id="Optimized" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 841.8897705 595.2755737" style="enable-background:new 0 0 841.8897705 595.2755737;" xml:space="preserve">
<polygon class={cssClass} style="fill: #020203;" points="512.5472412,488.2385864 542.3688354,537.3009033 558.0941772,491.724762 "/>
<path class={cssClass} style="fill:#020203;" d="M277.7966003,213.2223663c-1.1176147-1.0975342,1.4554749,8.9684143,5.0365906,12.5897064
c6.3496399,6.4062805,11.3096008,7.23172,13.9502563,7.3376465c5.8437805,0.2337646,13.0553894-1.4554596,17.3377991-3.2506104
c4.1435852-1.767746,11.404541-5.4748993,14.1529236-10.8822479c0.5825195-1.1559601,2.1731262-3.0972137,1.1742859-7.8927765
c-0.7579041-3.6888733-3.1064148-4.980011-5.9698181-4.7754517c-2.8634644,0.1935883-11.5323792,2.5055237-15.7253113,3.7948151
c-4.1947021,1.2728424-12.8344116,3.9025269-16.6000366,4.7188416
C287.3968811,215.6767426,279.1151123,214.4842987,277.7966003,213.2223663z"/>
<path class={cssClass} style="fill: #020203;" d="M383.9637451,333.5533752c-1.6582031-0.6026001-35.9649353-14.8140564-40.828064-17.1424255
c-3.979248-1.9137573-13.7365417-6.0391541-18.3276062-7.9127808c12.9312439-19.9383545,21.0942688-34.984314,22.1808472-37.276123
c2.0106506-4.1929321,15.697876-30.9757843,16.0174561-32.6248322c0.3104858-1.6709595,0.6994324-7.8434601,0.3981323-9.3098907
c-0.3013-1.495636-5.3196411,1.3787689-12.1331482,3.6889343c-6.8244629,2.3009491-19.7940674,10.7361145-24.8032837,11.7934723
c-5.0274658,1.048233-21.0942688,7.134964-29.3157349,9.8631897c-8.2214966,2.7283783-23.7732849,7.4745941-30.1704102,9.2021637
c-6.4062805,1.7275696-11.9980316,1.8645325-15.5810242,2.9511414c0,0,0.4766541,5.0183411,1.4280701,6.5231934
c0.9404755,1.5046997,4.329895,5.193634,8.270813,6.2235718c3.9408875,1.037262,10.4640198,0.6208801,13.4352112-0.0584412
c2.9693909-0.6903381,8.1137695-3.203125,8.8040161-4.3006287c0.6976929-1.1158447-0.3596802-4.5527039,0.8144836-5.5917969
c1.1852417-1.0281067,16.8429565-4.6878052,22.754303-6.4738464c5.911377-1.8170166,28.5396118-9.6112061,31.6076355-9.2130585
c-0.9715576,3.2231903-19.1731262,39.2757416-25.0351562,50.0319366
c-5.8639221,10.7544556-39.9259338,58.0690613-47.1777039,66.4074402
c-5.5041504,6.3386841-18.8425598,22.5588379-23.4628143,26.2185059c1.1650848,0.3214722,9.4249115-0.3870544,10.9296875-1.3184509
c9.3774872-5.7762756,24.9968414-25.219635,30.0261078-31.1419983
c14.9492188-17.5313721,28.0831604-35.9465942,38.4978638-51.7503967h0.0109558
c2.0288696,0.8454895,18.4335022,14.2113342,22.7140503,17.1734619c4.2806396,2.9602051,21.172821,12.3851318,24.832489,13.9483643
c3.6596985,1.5832825,17.7249756,8.0681152,18.3166809,5.8730469
C388.7592468,347.1237793,385.6236877,334.1834412,383.9637451,333.5533752z"/>
<path class={cssClass} style="fill-rule:evenodd;clip-rule:evenodd;fill:#020203;" d="M304.6889954,512.2145996
c3.2871399,2.0087891,6.3916626,3.6523438,9.8614197,5.2958984c6.9394836,3.4697876,14.7920837,7.1221313,22.2794495,9.8614502
c10.2266541,3.8349609,20.4532776,6.9395142,30.6799316,9.3135376c5.6611633,1.2783203,11.8701782,2.3740234,17.8966064,3.2871094
c0.5478516,0,16.8009033,2.0087891,20.0880432,2.0087891h16.4356689c6.3916321-0.5478516,12.4180603-0.9130859,18.8096924-1.8261719
c5.1133423-0.7304688,10.7745056-1.6435547,16.2530518-2.921875c4.0176086-0.9130859,8.2178345-1.8261719,12.2354431-3.1045532
c3.8349915-1.0957031,8.2178345-2.5566406,12.4180603-4.0175781c2.7392883-0.9130859,5.6611938-2.1914062,8.5830688-3.2871094
c2.374054-1.0957642,5.2959595-2.3740845,8.0352173-3.4697876c3.2871399-1.4609375,7.1221313-3.4697266,10.7745056-5.2958984
c2.9219055-1.4609985,6.2090149-3.2871704,9.3135681-5.1133423c2.3740234-1.2783508,7.8526001-5.4785767,10.7744751-5.4785767
c3.2871094,0,5.4785767,2.9219055,5.4785767,5.4785767c0,5.2959595-7.1221313,6.9395142-10.4093018,9.3135376
c-3.4697266,2.3740234-7.6699829,4.2002563-11.3223267,6.2090454c-7.3047485,3.8349609-14.7921143,7.1221313-21.9142151,9.8613892
c-9.3135681,3.4697266-19.5401917,6.756897-28.6711121,8.9483032c-3.4697571,0.7304688-6.9395142,1.6435547-10.4092712,2.1914062
c-1.8261719,0.3652344-20.818512,3.2871704-26.1144409,3.2871704h-24.1056519
c-6.3916626-0.5478516-13.1485291-1.2783203-19.5401917-2.1914673c-5.6611633-0.9130859-11.6875916-2.0087891-17.3487549-3.2871094
c-4.382843-0.9130859-9.1309204-2.1914062-13.3311462-3.4697266c-7.3047485-2.0088501-14.4268799-4.5654907-21.366394-7.3047485
c-12.6006775-4.7481079-25.7492065-10.9571533-38.1672668-19.1749878c-2.1914062-1.4609375-2.3740234-2.921875-2.3740234-4.5654297
c0-2.7392883,2.0087891-5.2959595,5.295929-5.2959595C297.7495117,507.4664917,303.5932922,511.6667175,304.6889954,512.2145996z"/>
<g>
<path class={cssClass} d="M639.8383789,180.4025879l-40.7682495-12.975708V48.3634644
c0-3.4697266-2.5567017-5.843811-5.843811-5.843811c-2.5567017,0-84.9176636,28.3059082-91.4918823,30.6799316
c-22.3344727,7.4448242-86.798645,29.7334595-86.798645,29.7334595l-0.000061,0.0002441
c-1.0336304,0.2964478-2.6376953,0.7871704-4.7411499,1.4482422L252.854248,48.8499756
c-1.1881714-0.4193726-2.43396,0.4620972-2.43396,1.7220459v2v104.723877
c-24.4053345,8.1710205-41.8085327,14.0198364-42.7017212,14.335083c-1.6435547,0.5478516-4.2002563,0.9130859-5.6611938,2.921875
c-0.7304688,0.7304688-0.9130859,2.0088501-1.2783203,2.921936V484.456543c0,0.3652344,0.1826172,0.5478516,0.1826172,0.7304688
c1.0957031,2.3740845,3.1044922,3.835022,5.2959595,3.835022c2.7392578,0,208.3677368-69.0298462,212.9332275-70.8560181
c0.2156372-0.0718994,0.458374-0.2409668,0.6971436-0.4380493l218.84198,69.75177
c1.1779175,0.3754883,2.3807373-0.50354,2.3807373-1.7399292V182.1427612
C641.1107178,181.3475952,640.5961304,180.6437988,639.8383789,180.4025879z M410.9730225,409.4003296l-199.0542603,66.2905273
V182.0402222l199.0542603-66.2905273V409.4003296z M587.9303589,55.6682129v108.2130737l-164.4921875-52.3546143
L587.9303589,55.6682129z M567.6870728,385.267334l-10.5206299-38.4284668l-60.5161133-18.3422241l-13.0134277,31.3044434
l-29.2919922-8.8861084l62.1779785-152.5870361l28.5085449,8.6360474l51.9385376,187.1876831L567.6870728,385.267334z"/>
<polygon class={cssClass} style="fill: #020203;" points="507.8375244,300.3788452 547.7670288,312.4828491 529.5562744,247.8833618 "/>
</g>
</svg>