Skip to content

Commit b1733d5

Browse files
authored
Merge pull request #12 from Lenni009/dev
Add translations
2 parents 16e535e + 60c8c9c commit b1733d5

22 files changed

+321
-35
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# image-compressor
2-
Compresses Images to below 10MB
2+
Compresses Images to below 10MB.

env.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

package-lock.json

Lines changed: 62 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"jszip": "^3.10.1",
1818
"pinia": "^2.1.6",
1919
"sass": "^1.64.1",
20-
"vue": "^3.3.4"
20+
"vue": "^3.3.4",
21+
"vue-i18n": "^9.8.0"
2122
},
2223
"devDependencies": {
2324
"@tsconfig/node20": "^20.1.2",

src/App.vue

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import type { FileObj } from './types/file';
88
import { computed, ref, watch } from 'vue';
99
import { useImageCompression } from './composables/useImageCompression';
1010
import { useZipCompression } from './composables/useZipCompression';
11+
import { useI18n } from './hooks/useI18n';
12+
13+
const { t } = useI18n();
1114
1215
const fileDataStore = useFileDataStore();
1316
const { files } = storeToRefs(fileDataStore);
@@ -60,32 +63,32 @@ watch(anyUncompressed, async (newVal) => {
6063
<template>
6164
<header>
6265
<NavBar />
63-
<h1 class="title">Image Compressor</h1>
66+
<h1 class="title">{{ t('translation.header') }}</h1>
6467
</header>
6568

6669
<main>
6770
<div class="explanation-wrapper">
68-
<p class="explanation">Compresses images to &lt; 10MB.</p>
71+
<p class="explanation">{{ t('translation.subtitle') }}</p>
6972
<a
7073
href="https://nomanssky.fandom.com/wiki/Special:Upload?multiple=true"
7174
role="button"
7275
target="_blank"
7376
rel="noopener noreferrer"
74-
>Open NMS Wiki Image Upload</a
77+
>{{ t('translation.buttonwiki') }}</a
7578
>
7679
</div>
77-
<h2 class="subheading">Input</h2>
80+
<h2 class="subheading">{{ t('translation.input') }}</h2>
7881
<FileUpload />
7982

80-
<h2 class="subheading">File List</h2>
83+
<h2 class="subheading">{{ t('translation.filelist') }}</h2>
8184
<div class="buttons">
8285
<button
8386
:aria-busy="isCompressing"
8487
:class="{ 'is-success': files.length && !anyUncompressed }"
8588
:disabled="!files.length || !anyUncompressed"
8689
@click="compressFiles"
8790
>
88-
{{ files.length && !anyUncompressed ? 'All compressed!' : 'Compress' }}
91+
{{ files.length && !anyUncompressed ? t('translation.allcompressed') : t('translation.compress') }}
8992
</button>
9093
<a
9194
:aria-busy="isZipCompressing"
@@ -94,21 +97,21 @@ watch(anyUncompressed, async (newVal) => {
9497
role="button"
9598
download
9699
>
97-
Download ZIP
100+
{{ t('translation.downloadzip') }}
98101
</a>
99102
<button
100103
:disabled="!files.length"
101104
class="secondary"
102105
@click="files = []"
103106
>
104-
Clear List
107+
{{ t('translation.clearlist') }}
105108
</button>
106109
</div>
107110
<div class="file-list">
108111
<FileItem
109112
v-for="file in files"
110-
:key="file.id"
111113
:file-obj="file"
114+
:key="file.id"
112115
@remove="removeItem(file)"
113116
/>
114117
</div>

src/components/FileItem.vue

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<script setup lang="ts">
22
import { computed, ref, watchEffect } from 'vue';
33
import type { FileObj } from '../types/file';
4+
import { useI18n } from '../hooks/useI18n';
5+
6+
const { t } = useI18n();
47
58
const props = defineProps<{
69
fileObj: FileObj;
@@ -33,20 +36,26 @@ const computeFileSize = (size: number) =>
3336
>
3437
<img
3538
:src="fileData"
36-
class="preview"
3739
alt="Image preview"
40+
class="preview"
3841
width="200"
3942
/>
4043
</a>
4144
<div>
42-
<div><span class="field-title">Name:</span> {{ fileObj.file.name }}</div>
43-
<div><span class="field-title">Original Size:</span> {{ computeFileSize(orgSize) }}MB</div>
44-
<div v-if="compSize"><span class="field-title">Compressed Size:</span> {{ computeFileSize(compSize) }}MB</div>
45+
<div>
46+
<span class="field-title">{{ t('translation.name') }}</span> {{ fileObj.file.name }}
47+
</div>
48+
<div>
49+
<span class="field-title">{{ t('translation.originalsize') }}</span> {{ computeFileSize(orgSize) }}MB
50+
</div>
51+
<div v-if="compSize">
52+
<span class="field-title">{{ t('translation.compressedsize') }}</span> {{ computeFileSize(compSize) }}MB
53+
</div>
4554
<div
4655
v-if="fileObj.isTooLarge"
4756
class="error"
4857
>
49-
<span class="field-title">Error:</span> File is too large!
58+
<span class="field-title">{{ t('translation.error') }}</span> {{ t('translation.filetoolarge') }}
5059
</div>
5160
</div>
5261
<a
@@ -55,7 +64,7 @@ const computeFileSize = (size: number) =>
5564
:href="fileObj.isCompressed ? fileData : undefined"
5665
role="button"
5766
download
58-
>Download</a
67+
>{{ t('translation.download') }}</a
5968
>
6069
<button
6170
class="secondary delete-button"

src/components/FileUpload.vue

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { ref } from 'vue';
33
import { useFileDataStore } from '../stores/fileData';
44
import { storeToRefs } from 'pinia';
55
import type { FileObj } from '../types/file';
6+
import { useI18n } from '../hooks/useI18n';
67
78
const dragActive = ref(false);
89
10+
const { t } = useI18n();
11+
912
let id = 0;
1013
1114
const fileDataStore = useFileDataStore();
@@ -39,19 +42,19 @@ function addFiles(uploadedFiles: FileList) {
3942

4043
<template>
4144
<label
42-
for="fileUpload"
43-
class="drop-container"
4445
:class="{ 'drag-active': dragActive }"
46+
class="drop-container"
47+
for="fileUpload"
4548
@dragenter="dragActive = true"
4649
@dragleave="dragActive = false"
47-
@drop.prevent="dropFile"
4850
@dragover.prevent
51+
@drop.prevent="dropFile"
4952
>
50-
<span class="drop-title">Drop files here</span>
53+
<span class="drop-title">{{ t('translation.dropfiles') }}</span>
5154
<input
52-
type="file"
5355
id="fileUpload"
5456
multiple
57+
type="file"
5558
@change="uploadFile"
5659
/>
5760
</label>

src/components/NavBar.vue

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
11
<script setup lang="ts">
2+
import { watch, ref } from 'vue';
3+
import { useI18n } from '../hooks/useI18n';
24
import ThemeSwitch from './ThemeSwitch.vue';
5+
6+
const { t, locale } = useI18n();
7+
8+
type Locales = typeof locale.value;
9+
10+
const selectedLocale = ref<Locales>(locale.value);
11+
12+
watch(selectedLocale, (newVal) => {
13+
locale.value = newVal;
14+
localStorage.setItem('lang', newVal);
15+
});
316
</script>
417

518
<template>
619
<nav>
720
<ul>
821
<li>
9-
<a href="..">&larr; View other pages</a>
22+
<a
23+
href=".."
24+
:title="t('translation.viewother')"
25+
>← {{ t('translation.viewother') }}</a
26+
>
1027
</li>
1128
</ul>
1229
<ul>
30+
<li>
31+
<select v-model="selectedLocale">
32+
<option
33+
v-for="locale in $i18n.availableLocales"
34+
:key="`locale-${locale}`"
35+
:value="locale"
36+
>
37+
{{ locale }}
38+
</option>
39+
</select>
40+
</li>
1341
<li>
1442
<ThemeSwitch />
1543
</li>

src/components/ThemeSwitch.vue

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
<script setup lang="ts">
2+
import { useI18n } from '../hooks/useI18n';
3+
4+
const { t } = useI18n();
5+
26
// determine preferred theme and update the html element with the respective tag
37
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
48
switchTheme(prefersDark ? 'dark' : 'light');
59
610
function switchTheme(theme: string | undefined = undefined) {
711
const currentTheme = document.documentElement.dataset.theme;
8-
const computedNewTheme = currentTheme == 'dark' ? 'light' : 'dark';
12+
const computedNewTheme = currentTheme === 'dark' ? 'light' : 'dark';
913
const newTheme = theme ?? computedNewTheme;
1014
document.documentElement.dataset.theme = newTheme;
1115
}
1216
</script>
1317

1418
<template>
15-
<button
16-
role="button"
17-
class="themeswitcher"
18-
id="themeSwitch"
19-
@click="switchTheme()"
20-
>
21-
Switch Theme
22-
</button>
19+
<div style="text-align: right">
20+
<button
21+
class="themeswitcher"
22+
id="themeSwitch"
23+
style="width: auto"
24+
@click="switchTheme()"
25+
>
26+
{{ t('translation.switchtheme') }}
27+
</button>
28+
</div>
2329
</template>

src/hooks/useI18n.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { i18n, messages } from '../i18n';
2+
3+
// inspired by https://stackoverflow.com/questions/58434389/typescript-deep-keyof-of-a-nested-object
4+
5+
type Join<FirstType, SecondType> = FirstType extends string | number
6+
? SecondType extends string | number
7+
? `${FirstType}${'' extends SecondType ? '' : '.'}${SecondType}`
8+
: never
9+
: never;
10+
11+
/**
12+
* Helper type that transforms an object tree into a union type of all possibles leaves.
13+
*/
14+
type Leaves<ObjectType> = ObjectType extends Record<string, unknown>
15+
? // eslint-disable-next-line @typescript-eslint/no-unused-vars
16+
{ [Key in keyof ObjectType]-?: Join<Key, Leaves<ObjectType[Key]>> }[keyof ObjectType]
17+
: '';
18+
19+
export type I18NLeaves = Leaves<(typeof messages)['Español']>;
20+
21+
// This function adds type safety to the i18n t function.
22+
export function useI18n() {
23+
// eslint-disable-next-line @typescript-eslint/unbound-method
24+
const { t, te, d, n, tm, rt, ...globalApi } = i18n.global;
25+
26+
type RemoveFirstFromTuple<T extends unknown[]> = ((...b: T) => void) extends (...b: infer I) => void ? I : [];
27+
28+
const typedT = t as (...args: [I18NLeaves, ...Partial<RemoveFirstFromTuple<Parameters<typeof t>>>]) => string;
29+
30+
return {
31+
t: typedT.bind(i18n),
32+
d: d.bind(i18n),
33+
te: te.bind(i18n),
34+
tm: tm.bind(i18n),
35+
rt: rt.bind(i18n),
36+
n: n.bind(i18n),
37+
...globalApi,
38+
};
39+
}

0 commit comments

Comments
 (0)