Skip to content

Commit

Permalink
Merge pull request #69 from NMSCD/dev
Browse files Browse the repository at this point in the history
add i18n; bump deps
  • Loading branch information
Lenni009 authored Dec 30, 2024
2 parents 3478123 + 8714503 commit a3af410
Show file tree
Hide file tree
Showing 16 changed files with 1,137 additions and 481 deletions.
1,327 changes: 879 additions & 448 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@
},
"dependencies": {
"@picocss/pico": "^2.0.6",
"vue": "^3.5.13"
"vue": "^3.5.13",
"vue-i18n": "^11.0.1"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"@vueuse/core": "^12.2.0",
"prettier": "^3.4.2",
"typescript": "5.6.3",
"vite": "^6.0.5",
"vue-tsc": "^2.1.10"
"sass-embedded": "^1.83.0",
"typescript": "5.7.2",
"vite": "^6.0.6",
"vue-tsc": "^2.2.0"
}
}
44 changes: 27 additions & 17 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import TraitInput from './components/TraitInput.vue';
import StatInput from './components/StatInput.vue';
import { isNumberInput } from './helpers/validation';
import HelpPopup from './components/HelpPopup.vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const levelUps = [4, 8, 15, 25, 30, 35, 40, 45, 50, 55]; // NoSonar these are given by the game
Expand All @@ -18,8 +21,14 @@ const traitValues = ref<number[]>([0]);
const addTrait = () => traitValues.value.push(0);
const removeTrait = (index: number) => (traitValues.value = traitValues.value.filter((_, idx) => idx !== index));
const stats = ['Combat', 'Exploration', 'Industrial', 'Trade'];
const statValues = ref<number[]>(Array.from({ length: stats.length }, () => 0));
const stats = computed(() => [
t('translation.combat'),
t('translation.exploration'),
t('translation.industrial'),
t('translation.trade'),
]);
const statValues = ref<number[]>(Array.from({ length: stats.value.length }, () => 0));
const expeditionCount = ref(0);
const isExpeditionCountValid = computed(() => isNumberInput(expeditionCount.value.toString()));
Expand Down Expand Up @@ -62,14 +71,14 @@ const progressClassName = computed(() => {
<template>
<header>
<NavBar />
<h1 class="text-center">Frigate Calculator</h1>
<h1 class="text-center">{{ t('translation.title') }}</h1>
</header>

<main>
<form @submit.prevent>
<fieldset>
<legend>
<span>Raw Frigate Stats</span>
<span>{{ t('translation.statsField') }}</span>
<HelpPopup input="stats" />
</legend>

Expand All @@ -80,12 +89,12 @@ const progressClassName = computed(() => {
:placeholder="stat"
/>
</div>
<p>Total Base Stats: {{ combinedStats }}</p>
<p>{{ t('translation.totalbaseStats') }} {{ combinedStats }}</p>
</fieldset>

<fieldset>
<legend>
<span>Traits (except Fuel & Duration)</span>
<span>{{ t('translation.traitsField') }}</span>
<HelpPopup input="traits" />
</legend>
<div class="trait-list">
Expand All @@ -100,28 +109,29 @@ const progressClassName = computed(() => {
class="add-trait-button"
@click="addTrait"
>
Add Trait
{{ t('translation.addtrait') }}
</button>
<p>Total Bonus Points: {{ combinedTraits }}</p>
<p>{{ t('translation.totalBonusPoints') }} {{ combinedTraits }}</p>
</fieldset>

<fieldset>
<legend>
<span>Amount of Expeditions</span>
<span>{{ t('translation.expAmmount') }}</span>
<HelpPopup input="expeditions" />
</legend>
<input
v-model.trim.number="expeditionCount"
:aria-invalid="!isExpeditionCountValid || undefined"
type="text"
placeholder="Expeditions"
:placeholder="t('translation.expeditions')"
/>
<p>Total Rank-Ups: {{ calculatedExpeditionLevel }}</p>

<p>{{ t('translation.totalRankUps') }} {{ calculatedExpeditionLevel }}</p>
</fieldset>
</form>

<p>
Frigate score (-5 – 14): <output class="text-bold">{{ calculatedBaseStat }}</output>
{{ t('translation.frigscore') }} <output class="text-bold">{{ calculatedBaseStat }}</output>
</p>

<progress
Expand All @@ -136,7 +146,7 @@ const progressClassName = computed(() => {

<footer>
<div class="credits text-center">
Forked from nms-frigate-calc by
{{ t('translation.forkedfrom') }}
<a
href="https://github.com/gander/"
target="_blank"
Expand All @@ -146,7 +156,7 @@ const progressClassName = computed(() => {
(<a
href="https://nms.gander.tools/"
target="_blank"
>Website</a
>{{ t('translation.website') }}</a
>
|
<a
Expand All @@ -157,20 +167,20 @@ const progressClassName = computed(() => {
</div>

<div class="sources">
<p>Formula sources:</p>
<p>{{ t('translation.formula') }}</p>
<ul>
<li>
<a
href="https://steamcommunity.com/sharedfiles/filedetails/?id=1505175794"
target="_blank"
>Frigate Buyer's Guide - How to Pick the Best Ships (and avoid "Lemons")</a
>{{ t('translation.buyersguide') }}</a
>
</li>
<li>
<a
href="https://www.reddit.com/r/NoMansSkyTheGame/comments/knjokc/a_guide_to_evaluating_frigate_stats/"
target="_blank"
>A Guide to Evaluating Frigate Stats</a
>{{ t('translation.guide') }}</a
>
</li>
</ul>
Expand Down
27 changes: 20 additions & 7 deletions src/components/HelpPopup.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref, computed } from 'vue';
import expeditionHelp from '@/assets/expeditions.webp';
import statsHelp from '@/assets/stats.webp';
import traitsHelp from '@/assets/traits.webp';
import { useActiveElement } from '@vueuse/core';
import { useI18n } from '@/hooks/useI18n';
defineProps<{
const { t } = useI18n();
const props = defineProps<{
input: 'expeditions' | 'stats' | 'traits';
}>();
Expand All @@ -27,16 +30,26 @@ const showModal = () => {
openedOnce.value = true;
};
const closeModal = () => dialog.value?.close();
const translatedInput = computed(() => {
const translations: Record<string, string> = {
expeditions: t('translation.expeditions'),
stats: t('translation.stats'),
traits: t('translation.traits'),
};
return translations[props.input] ?? 'Unknown value';
});
</script>

<template>
<button
class="help-button"
data-tooltip="Help"
:data-tooltip="t('translation.help')"
@click="showModal"
>
<img
alt="Help"
:alt="t('translation.help')"
class="help-icon"
loading="lazy"
src="../assets/help.svg"
Expand All @@ -50,11 +63,11 @@ const closeModal = () => dialog.value?.close();
<article>
<header>
<button
aria-label="Close"
:aria-label="t('translation.close')"
class="close"
@click="closeModal"
></button>
<p class="text-bold">Where to find {{ input }}</p>
<p class="text-bold">{{ t('translation.wheretofind') }} {{ translatedInput }}</p>
</header>
<div class="transition-container">
<div
Expand All @@ -68,7 +81,7 @@ const closeModal = () => dialog.value?.close();
<Transition>
<img
v-if="openedOnce"
:alt="`Where to find ${input}`"
:alt="`${t('translation.wheretofind')} ${translatedInput}`"
:src="imageMapping[input]"
loading="lazy"
@load="isLoading = false"
Expand Down
26 changes: 24 additions & 2 deletions src/components/NavBar.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
<script setup lang="ts">
import { watch, ref } from 'vue';
import ThemeSwitch from './ThemeSwitch.vue';
import { useI18n } from '@/hooks/useI18n';
const { t, locale, availableLocales } = useI18n();
const selectedLocale = ref(locale.value);
watch(selectedLocale, (newVal) => {
locale.value = newVal;
localStorage.setItem('lang', newVal);
});
</script>

<template>
Expand All @@ -8,12 +18,24 @@ import ThemeSwitch from './ThemeSwitch.vue';
<li>
<a
href=".."
title="View other pages"
>← View other pages</a
:title="t('translation.viewother')"
>
← {{ t('translation.viewother') }}
</a>
</li>
</ul>
<ul>
<li>
<select v-model="selectedLocale">
<option
v-for="loc in availableLocales"
:key="`locale-${loc}`"
:value="loc"
>
{{ loc }}
</option>
</select>
</li>
<li>
<ThemeSwitch />
</li>
Expand Down
5 changes: 4 additions & 1 deletion src/components/ThemeSwitch.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const isDark = useDark({ attribute: 'data-theme' });
const toggleDark = useToggle(isDark);
</script>

<template>
<button @click="toggleDark()">Switch Theme</button>
<button @click="toggleDark()">{{ t('translation.switchtheme') }}</button>
</template>
5 changes: 4 additions & 1 deletion src/components/TraitInput.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<script setup lang="ts">
import { isNumberInput } from '@/helpers/validation';
import { computed } from 'vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const emit = defineEmits(['remove']);
Expand All @@ -14,7 +17,7 @@ const isValidInput = computed(() => isNumberInput(model.value.toString()));
<input
v-model.trim.number="model"
:aria-invalid="!isValidInput || undefined"
placeholder="Trait Value"
:placeholder="t('translation.traitvalue')"
type="text"
/>
<button
Expand Down
38 changes: 38 additions & 0 deletions src/hooks/useI18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { i18n, messages } from '@/i18n';

type Join<FirstType, SecondType> = FirstType extends string | number
? SecondType extends string | number
? `${FirstType}${'' extends SecondType ? '' : '.'}${SecondType}`
: never
: never;

/**
* Helper type that transforms an object tree into a union type of all possibles leaves.
*/
type Leaves<ObjectType> =
ObjectType extends Record<string, unknown>
? // eslint-disable-next-line @typescript-eslint/no-unused-vars
{ [Key in keyof ObjectType]-?: Join<Key, Leaves<ObjectType[Key]>> }[keyof ObjectType]
: '';

export type I18NLeaves = Leaves<(typeof messages)['Español']>;

// This function adds type safety to the i18n t function.
export function useI18n() {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { t, te, d, n, tm, rt, ...globalApi } = i18n.global;

type RemoveFirstFromTuple<T extends unknown[]> = ((...b: T) => void) extends (...b: infer I) => void ? I : [];

const typedT = t as (...args: [I18NLeaves, ...Partial<RemoveFirstFromTuple<Parameters<typeof t>>>]) => string;

return {
t: typedT.bind(i18n),
d: d.bind(i18n),
te: te.bind(i18n),
tm: tm.bind(i18n),
rt: rt.bind(i18n),
n: n.bind(i18n),
...globalApi,
};
}
5 changes: 5 additions & 0 deletions src/i18n/en-EN/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import translation from './translation';

export default {
translation,
};
29 changes: 29 additions & 0 deletions src/i18n/en-EN/translation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default {
viewother: 'View other pages',
switchtheme: 'Switch Theme',
title: 'Frigate Calculator',
statsField: 'Raw Frigate Stats',
traitsField: 'Traits (except Fuel & Duration)',
combat: 'Combat',
exploration: 'Exploration',
industrial: 'Industrial',
trade: 'Trade',
totalbaseStats: 'Total Base Stats:',
expAmmount: 'Amount of Expeditions',
totalRankUps: 'Total Rank-Ups:',
expeditions: 'Expeditions',
addtrait: 'Add Trait',
traitvalue: 'Trait Value',
totalBonusPoints: 'Total Bonus Points:',
frigscore: 'Frigate score (-5 – 14):',
forkedfrom: 'Forked from nms-frigate-calc by',
website: 'Website',
guide: 'A Guide to Evaluating Frigate Stats',
formula: 'Formula sources:',
buyersguide: 'Frigate Buyer\'s Guide - How to Pick the Best Ships (and avoid "Lemons")',
help: 'Help',
close: 'Close',
wheretofind: 'Where to find',
stats: 'Stats',
traits: 'Traits',
};
5 changes: 5 additions & 0 deletions src/i18n/es-ES/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import translation from './translation';

export default {
translation,
};
Loading

0 comments on commit a3af410

Please sign in to comment.