i18n: automatyczny język + ręczny przełącznik
This commit is contained in:
40
src/App.vue
40
src/App.vue
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { usePuzzleStore } from './stores/puzzle';
|
import { usePuzzleStore } from './stores/puzzle';
|
||||||
|
import { useI18n } from './composables/useI18n';
|
||||||
import GameBoard from './components/GameBoard.vue';
|
import GameBoard from './components/GameBoard.vue';
|
||||||
import LevelSelector from './components/LevelSelector.vue';
|
import LevelSelector from './components/LevelSelector.vue';
|
||||||
import StatusPanel from './components/StatusPanel.vue';
|
import StatusPanel from './components/StatusPanel.vue';
|
||||||
@@ -12,6 +13,7 @@ import FixedBar from './components/FixedBar.vue';
|
|||||||
|
|
||||||
// Main App Entry
|
// Main App Entry
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
|
const { t, locale, setLocale } = useI18n();
|
||||||
const showCustomModal = ref(false);
|
const showCustomModal = ref(false);
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
|
|
||||||
@@ -27,7 +29,11 @@ onMounted(() => {
|
|||||||
<FixedBar />
|
<FixedBar />
|
||||||
|
|
||||||
<header class="game-header">
|
<header class="game-header">
|
||||||
<h1>NONOGRAMS</h1>
|
<h1>{{ t('app.title') }}</h1>
|
||||||
|
<div class="lang-toggle">
|
||||||
|
<button class="lang-btn" :class="{ active: locale === 'pl' }" @click="setLocale('pl')">PL</button>
|
||||||
|
<button class="lang-btn" :class="{ active: locale === 'en' }" @click="setLocale('en')">EN</button>
|
||||||
|
</div>
|
||||||
<div class="underline"></div>
|
<div class="underline"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -96,6 +102,38 @@ h1 {
|
|||||||
box-shadow: 0 0 10px var(--primary-accent);
|
box-shadow: 0 0 10px var(--primary-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lang-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn.active {
|
||||||
|
border-color: var(--primary-accent);
|
||||||
|
box-shadow: 0 0 10px rgba(0, 242, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn:hover {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.game-layout {
|
.game-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const customSize = ref(10);
|
const customSize = ref(10);
|
||||||
const errorMsg = ref('');
|
const errorMsg = ref('');
|
||||||
@@ -11,7 +13,7 @@ const errorMsg = ref('');
|
|||||||
const confirm = () => {
|
const confirm = () => {
|
||||||
const size = parseInt(customSize.value);
|
const size = parseInt(customSize.value);
|
||||||
if (isNaN(size) || size < 5 || size > 100) {
|
if (isNaN(size) || size < 5 || size > 100) {
|
||||||
errorMsg.value = 'Rozmiar musi być między 5 a 100!';
|
errorMsg.value = t('custom.sizeError');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,8 +25,8 @@ const confirm = () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal-overlay" @click.self="emit('close')">
|
<div class="modal-overlay" @click.self="emit('close')">
|
||||||
<div class="modal glass-panel">
|
<div class="modal glass-panel">
|
||||||
<h2>GRA WŁASNA</h2>
|
<h2>{{ t('custom.title') }}</h2>
|
||||||
<p>Wprowadź rozmiar siatki (5 - 100):</p>
|
<p>{{ t('custom.prompt') }}</p>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
@@ -39,8 +41,8 @@ const confirm = () => {
|
|||||||
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
|
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-neon secondary" @click="emit('close')">Anuluj</button>
|
<button class="btn-neon secondary" @click="emit('close')">{{ t('custom.cancel') }}</button>
|
||||||
<button class="btn-neon" @click="confirm">Start</button>
|
<button class="btn-neon" @click="confirm">{{ t('custom.start') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
import { useTimer } from '@/composables/useTimer';
|
import { useTimer } from '@/composables/useTimer';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
const { formatTime } = useTimer();
|
const { formatTime } = useTimer();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const isVisible = ref(false);
|
const isVisible = ref(false);
|
||||||
const isProgressVisible = ref(true); // Toggle for progress percentage
|
const isProgressVisible = ref(true); // Toggle for progress percentage
|
||||||
@@ -33,14 +35,14 @@ const toggleVisibility = () => {
|
|||||||
<div id="fixed-bar" :class="{ visible: isVisible }">
|
<div id="fixed-bar" :class="{ visible: isVisible }">
|
||||||
<div class="fixed-content">
|
<div class="fixed-content">
|
||||||
<div class="fixed-stat">
|
<div class="fixed-stat">
|
||||||
<span>Czas:</span>
|
<span>{{ t('fixed.time') }}</span>
|
||||||
<span>{{ formattedTime }}</span>
|
<span>{{ formattedTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fixed-stat">
|
<div class="fixed-stat">
|
||||||
<span>Postęp:</span>
|
<span>{{ t('fixed.progress') }}</span>
|
||||||
<span style="min-width: 60px; text-align: right;">{{ progressText }}</span>
|
<span style="min-width: 60px; text-align: right;">{{ progressText }}</span>
|
||||||
<button class="btn-eye" @click="toggleVisibility" :title="isProgressVisible ? 'Ukryj' : 'Pokaż'">
|
<button class="btn-eye" @click="toggleVisibility" :title="isProgressVisible ? t('fixed.hide') : t('fixed.show')">
|
||||||
<span v-if="isProgressVisible">👁️</span>
|
<span v-if="isProgressVisible">👁️</span>
|
||||||
<span v-else>🔒</span>
|
<span v-else>🔒</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
function handleNewRandom() {
|
function handleNewRandom() {
|
||||||
// If currently custom, regenerate custom.
|
// If currently custom, regenerate custom.
|
||||||
@@ -16,9 +18,9 @@ function handleNewRandom() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="game-actions">
|
<div class="game-actions">
|
||||||
<button class="btn-neon secondary" @click="store.resetGame">RESET</button>
|
<button class="btn-neon secondary" @click="store.resetGame">{{ t('actions.reset') }}</button>
|
||||||
<button class="btn-neon secondary" @click="handleNewRandom">NOWA LOSOWA</button>
|
<button class="btn-neon secondary" @click="handleNewRandom">{{ t('actions.random') }}</button>
|
||||||
<button class="btn-neon secondary" @click="store.undo">COFNIJ</button>
|
<button class="btn-neon secondary" @click="store.undo">{{ t('actions.undo') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useSolver } from '@/composables/useSolver';
|
import { useSolver } from '@/composables/useSolver';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
@@ -9,6 +10,7 @@ const {
|
|||||||
togglePlay,
|
togglePlay,
|
||||||
changeSpeed
|
changeSpeed
|
||||||
} = useSolver();
|
} = useSolver();
|
||||||
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -17,15 +19,15 @@ const {
|
|||||||
|
|
||||||
<div class="guide-controls">
|
<div class="guide-controls">
|
||||||
<button class="btn-neon small" @click="togglePlay" :class="{ active: isPlaying }">
|
<button class="btn-neon small" @click="togglePlay" :class="{ active: isPlaying }">
|
||||||
{{ isPlaying ? 'PAUSE' : 'PLAY' }}
|
{{ isPlaying ? t('guide.pause') : t('guide.play') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn-neon small" @click="step" :disabled="isPlaying">
|
<button class="btn-neon small" @click="step" :disabled="isPlaying">
|
||||||
STEP
|
{{ t('guide.step') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn-neon small" @click="changeSpeed">
|
<button class="btn-neon small" @click="changeSpeed">
|
||||||
SPEED: {{ speedLabel }}
|
{{ t('guide.speed') }}: {{ speedLabel }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const levels = [
|
const levels = computed(() => [
|
||||||
{ id: 'easy', label: 'ŁATWY 5X5' },
|
{ id: 'easy', label: t('level.easy') },
|
||||||
{ id: 'medium', label: 'ŚREDNI 10X10' },
|
{ id: 'medium', label: t('level.medium') },
|
||||||
{ id: 'hard', label: 'TRUDNY 15X15' }
|
{ id: 'hard', label: t('level.hard') }
|
||||||
];
|
]);
|
||||||
|
|
||||||
const emit = defineEmits(['open-custom', 'toggle-guide']);
|
const emit = defineEmits(['open-custom', 'toggle-guide']);
|
||||||
</script>
|
</script>
|
||||||
@@ -29,14 +32,14 @@ const emit = defineEmits(['open-custom', 'toggle-guide']);
|
|||||||
:class="{ active: store.currentLevelId === 'custom' }"
|
:class="{ active: store.currentLevelId === 'custom' }"
|
||||||
@click="emit('open-custom')"
|
@click="emit('open-custom')"
|
||||||
>
|
>
|
||||||
WŁASNY
|
{{ t('level.custom') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn-neon guide-btn"
|
class="btn-neon guide-btn"
|
||||||
@click="emit('toggle-guide')"
|
@click="emit('toggle-guide')"
|
||||||
>
|
>
|
||||||
GUIDE ❓
|
{{ t('level.guide') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
import { useTimer } from '@/composables/useTimer';
|
import { useTimer } from '@/composables/useTimer';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
const { formatTime } = useTimer();
|
const { formatTime } = useTimer();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const formattedTime = computed(() => formatTime(store.elapsedTime));
|
const formattedTime = computed(() => formatTime(store.elapsedTime));
|
||||||
const progressText = computed(() => `${store.progressPercentage.toFixed(3)}%`);
|
const progressText = computed(() => `${store.progressPercentage.toFixed(3)}%`);
|
||||||
@@ -13,17 +15,17 @@ const progressText = computed(() => `${store.progressPercentage.toFixed(3)}%`);
|
|||||||
<template>
|
<template>
|
||||||
<div class="status-panel glass-panel">
|
<div class="status-panel glass-panel">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="label">CZAS</span>
|
<span class="label">{{ t('status.time') }}</span>
|
||||||
<span class="value">{{ formattedTime }}</span>
|
<span class="value">{{ formattedTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="label">RUCHY</span>
|
<span class="label">{{ t('status.moves') }}</span>
|
||||||
<span class="value">{{ store.moves }}</span>
|
<span class="value">{{ store.moves }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="label">POSTĘP</span>
|
<span class="label">{{ t('status.progress') }}</span>
|
||||||
<div class="progress-wrapper">
|
<div class="progress-wrapper">
|
||||||
<span class="value small">{{ progressText }}</span>
|
<span class="value small">{{ progressText }}</span>
|
||||||
<span class="eye-icon">👁️</span>
|
<span class="eye-icon">👁️</span>
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { Fireworks } from 'fireworks-js';
|
import { Fireworks } from 'fireworks-js';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
|
const { t } = useI18n();
|
||||||
const fireworksRef = ref(null);
|
const fireworksRef = ref(null);
|
||||||
let fireworksInstance = null;
|
let fireworksInstance = null;
|
||||||
let audioContext = null;
|
let audioContext = null;
|
||||||
@@ -110,18 +112,18 @@ onUnmounted(() => {
|
|||||||
<div class="modal-overlay" @click.self="handleClose">
|
<div class="modal-overlay" @click.self="handleClose">
|
||||||
<div ref="fireworksRef" class="fireworks-layer"></div>
|
<div ref="fireworksRef" class="fireworks-layer"></div>
|
||||||
<div class="modal glass-panel">
|
<div class="modal glass-panel">
|
||||||
<h2>GRATULACJE!</h2>
|
<h2>{{ t('win.title') }}</h2>
|
||||||
<p>Rozwiązałeś zagadkę!</p>
|
<p>{{ t('win.message') }}</p>
|
||||||
|
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span>Czas:</span>
|
<span>{{ t('win.time') }}</span>
|
||||||
<strong>{{ store.elapsedTime }}s</strong>
|
<strong>{{ store.elapsedTime }}s</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-neon" @click="store.resetGame">Zagraj Ponownie</button>
|
<button class="btn-neon" @click="store.resetGame">{{ t('win.playAgain') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
113
src/composables/useI18n.js
Normal file
113
src/composables/useI18n.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
const detectLocale = () => {
|
||||||
|
if (typeof navigator === 'undefined') return 'en';
|
||||||
|
const browserLocale = (navigator.languages && navigator.languages[0]) || navigator.language || 'en';
|
||||||
|
const short = browserLocale.toLowerCase().split('-')[0];
|
||||||
|
return short === 'pl' ? 'pl' : 'en';
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
pl: {
|
||||||
|
'app.title': 'Nonograms',
|
||||||
|
'level.easy': 'ŁATWY 5X5',
|
||||||
|
'level.medium': 'ŚREDNI 10X10',
|
||||||
|
'level.hard': 'TRUDNY 15X15',
|
||||||
|
'level.custom': 'WŁASNY',
|
||||||
|
'level.guide': 'PODPOWIEDŹ ❓',
|
||||||
|
'actions.reset': 'RESET',
|
||||||
|
'actions.random': 'NOWA LOSOWA',
|
||||||
|
'actions.undo': 'COFNIJ',
|
||||||
|
'status.time': 'CZAS',
|
||||||
|
'status.moves': 'RUCHY',
|
||||||
|
'status.progress': 'POSTĘP',
|
||||||
|
'fixed.time': 'Czas:',
|
||||||
|
'fixed.progress': 'Postęp:',
|
||||||
|
'fixed.hide': 'Ukryj',
|
||||||
|
'fixed.show': 'Pokaż',
|
||||||
|
'guide.play': 'START',
|
||||||
|
'guide.pause': 'PAUZA',
|
||||||
|
'guide.step': 'KROK',
|
||||||
|
'guide.speed': 'SZYBKOŚĆ',
|
||||||
|
'guide.waiting': 'Oczekiwanie...',
|
||||||
|
'guide.solved': 'Rozwiązane!',
|
||||||
|
'custom.title': 'GRA WŁASNA',
|
||||||
|
'custom.prompt': 'Wprowadź rozmiar siatki (5 - 100):',
|
||||||
|
'custom.cancel': 'Anuluj',
|
||||||
|
'custom.start': 'Start',
|
||||||
|
'custom.sizeError': 'Rozmiar musi być między 5 a 100!',
|
||||||
|
'win.title': 'GRATULACJE!',
|
||||||
|
'win.message': 'Rozwiązałeś zagadkę!',
|
||||||
|
'win.time': 'Czas:',
|
||||||
|
'win.playAgain': 'Zagraj Ponownie'
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
'app.title': 'Nonograms',
|
||||||
|
'level.easy': 'EASY 5X5',
|
||||||
|
'level.medium': 'MEDIUM 10X10',
|
||||||
|
'level.hard': 'HARD 15X15',
|
||||||
|
'level.custom': 'CUSTOM',
|
||||||
|
'level.guide': 'GUIDE ❓',
|
||||||
|
'actions.reset': 'RESET',
|
||||||
|
'actions.random': 'NEW RANDOM',
|
||||||
|
'actions.undo': 'UNDO',
|
||||||
|
'status.time': 'TIME',
|
||||||
|
'status.moves': 'MOVES',
|
||||||
|
'status.progress': 'PROGRESS',
|
||||||
|
'fixed.time': 'Time:',
|
||||||
|
'fixed.progress': 'Progress:',
|
||||||
|
'fixed.hide': 'Hide',
|
||||||
|
'fixed.show': 'Show',
|
||||||
|
'guide.play': 'PLAY',
|
||||||
|
'guide.pause': 'PAUSE',
|
||||||
|
'guide.step': 'STEP',
|
||||||
|
'guide.speed': 'SPEED',
|
||||||
|
'guide.waiting': 'Waiting...',
|
||||||
|
'guide.solved': 'Solved!',
|
||||||
|
'custom.title': 'CUSTOM GAME',
|
||||||
|
'custom.prompt': 'Enter grid size (5 - 100):',
|
||||||
|
'custom.cancel': 'Cancel',
|
||||||
|
'custom.start': 'Start',
|
||||||
|
'custom.sizeError': 'Size must be between 5 and 100!',
|
||||||
|
'win.title': 'CONGRATULATIONS!',
|
||||||
|
'win.message': 'You solved the puzzle!',
|
||||||
|
'win.time': 'Time:',
|
||||||
|
'win.playAgain': 'Play Again'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const locale = ref(detectLocale());
|
||||||
|
|
||||||
|
const format = (text, params = {}) => {
|
||||||
|
return text.replace(/\{(\w+)\}/g, (_, key) => {
|
||||||
|
const value = params[key];
|
||||||
|
return value === undefined ? `{${key}}` : String(value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = (key, params) => {
|
||||||
|
const lang = messages[locale.value] || messages.en;
|
||||||
|
const value = lang[key] || messages.en[key] || key;
|
||||||
|
return typeof value === 'string' ? format(value, params) : key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLocale = (value) => {
|
||||||
|
locale.value = messages[value] ? value : 'en';
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.lang = locale.value;
|
||||||
|
document.title = t('app.title');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.lang = locale.value;
|
||||||
|
document.title = messages[locale.value]?.['app.title'] || 'Nonograms';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useI18n() {
|
||||||
|
return {
|
||||||
|
locale: computed(() => locale.value),
|
||||||
|
t,
|
||||||
|
setLocale
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { ref, computed, onUnmounted } from 'vue';
|
import { ref, computed, onUnmounted } from 'vue';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
export function useSolver() {
|
export function useSolver() {
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
const isPlaying = ref(false);
|
const isPlaying = ref(false);
|
||||||
const isProcessing = ref(false);
|
const isProcessing = ref(false);
|
||||||
const speedIndex = ref(0);
|
const speedIndex = ref(0);
|
||||||
const speeds = [1000, 500, 250, 125];
|
const speeds = [1000, 500, 250, 125];
|
||||||
const speedLabels = ['x1', 'x2', 'x3', 'x4'];
|
const speedLabels = ['x1', 'x2', 'x3', 'x4'];
|
||||||
const statusText = ref('Oczekiwanie...');
|
const statusText = ref(t('guide.waiting'));
|
||||||
|
|
||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
let worker = null;
|
let worker = null;
|
||||||
@@ -18,7 +20,7 @@ export function useSolver() {
|
|||||||
function step() {
|
function step() {
|
||||||
if (store.isGameWon) {
|
if (store.isGameWon) {
|
||||||
pause();
|
pause();
|
||||||
statusText.value = "Rozwiązane!";
|
statusText.value = t('guide.solved');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isProcessing.value) return;
|
if (isProcessing.value) return;
|
||||||
@@ -28,7 +30,7 @@ export function useSolver() {
|
|||||||
const playerGrid = store.playerGrid.map(row => row.slice());
|
const playerGrid = store.playerGrid.map(row => row.slice());
|
||||||
const solution = store.solution.map(row => row.slice());
|
const solution = store.solution.map(row => row.slice());
|
||||||
const id = ++requestId;
|
const id = ++requestId;
|
||||||
worker.postMessage({ id, playerGrid, solution });
|
worker.postMessage({ id, playerGrid, solution, locale: locale.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePlay() {
|
function togglePlay() {
|
||||||
|
|||||||
@@ -1,5 +1,45 @@
|
|||||||
import { calculateHints } from '../utils/puzzleUtils.js';
|
import { calculateHints } from '../utils/puzzleUtils.js';
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
pl: {
|
||||||
|
'worker.solved': 'Rozwiązane!',
|
||||||
|
'worker.logicRow': 'Logika: Wiersz {row}, Kolumna {col} -> {state}',
|
||||||
|
'worker.logicCol': 'Logika: Kolumna {col}, Wiersz {row} -> {state}',
|
||||||
|
'worker.guess': 'Zgadywanie: Wiersz {row}, Kolumna {col}',
|
||||||
|
'worker.done': 'Koniec!',
|
||||||
|
'worker.state.filled': 'Pełne',
|
||||||
|
'worker.state.empty': 'Puste'
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
'worker.solved': 'Solved!',
|
||||||
|
'worker.logicRow': 'Logic: Row {row}, Column {col} -> {state}',
|
||||||
|
'worker.logicCol': 'Logic: Column {col}, Row {row} -> {state}',
|
||||||
|
'worker.guess': 'Guessing: Row {row}, Column {col}',
|
||||||
|
'worker.done': 'Done!',
|
||||||
|
'worker.state.filled': 'Filled',
|
||||||
|
'worker.state.empty': 'Empty'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveLocale = (value) => {
|
||||||
|
if (!value) return 'en';
|
||||||
|
const short = String(value).toLowerCase().split('-')[0];
|
||||||
|
return short === 'pl' ? 'pl' : 'en';
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = (text, params = {}) => {
|
||||||
|
return text.replace(/\{(\w+)\}/g, (_, key) => {
|
||||||
|
const value = params[key];
|
||||||
|
return value === undefined ? `{${key}}` : String(value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = (locale, key, params) => {
|
||||||
|
const lang = messages[locale] || messages.en;
|
||||||
|
const value = lang[key] || messages.en[key] || key;
|
||||||
|
return typeof value === 'string' ? format(value, params) : key;
|
||||||
|
};
|
||||||
|
|
||||||
const getPermutations = (length, hints) => {
|
const getPermutations = (length, hints) => {
|
||||||
const results = [];
|
const results = [];
|
||||||
const recurse = (index, hintIndex, currentLine) => {
|
const recurse = (index, hintIndex, currentLine) => {
|
||||||
@@ -73,9 +113,9 @@ const isSolved = (grid, solution) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStep = (playerGrid, solution) => {
|
const handleStep = (playerGrid, solution, locale) => {
|
||||||
if (isSolved(playerGrid, solution)) {
|
if (isSolved(playerGrid, solution)) {
|
||||||
return { type: 'done', statusText: 'Rozwiązane!' };
|
return { type: 'done', statusText: t(locale, 'worker.solved') };
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = solution.length;
|
const size = solution.length;
|
||||||
@@ -86,12 +126,13 @@ const handleStep = (playerGrid, solution) => {
|
|||||||
const hints = rowHints[r];
|
const hints = rowHints[r];
|
||||||
const result = solveLineLogic(rowLine, hints, size);
|
const result = solveLineLogic(rowLine, hints, size);
|
||||||
if (result.index !== -1) {
|
if (result.index !== -1) {
|
||||||
|
const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty');
|
||||||
return {
|
return {
|
||||||
type: 'move',
|
type: 'move',
|
||||||
r,
|
r,
|
||||||
c: result.index,
|
c: result.index,
|
||||||
state: result.state,
|
state: result.state,
|
||||||
statusText: `Logika: Wiersz ${r + 1}, Kolumna ${result.index + 1} -> ${result.state === 1 ? 'Pełne' : 'Puste'}`
|
statusText: t(locale, 'worker.logicRow', { row: r + 1, col: result.index + 1, state: stateLabel })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,12 +143,13 @@ const handleStep = (playerGrid, solution) => {
|
|||||||
const hints = colHints[c];
|
const hints = colHints[c];
|
||||||
const result = solveLineLogic(colLine, hints, size);
|
const result = solveLineLogic(colLine, hints, size);
|
||||||
if (result.index !== -1) {
|
if (result.index !== -1) {
|
||||||
|
const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty');
|
||||||
return {
|
return {
|
||||||
type: 'move',
|
type: 'move',
|
||||||
r: result.index,
|
r: result.index,
|
||||||
c,
|
c,
|
||||||
state: result.state,
|
state: result.state,
|
||||||
statusText: `Logika: Kolumna ${c + 1}, Wiersz ${result.index + 1} -> ${result.state === 1 ? 'Pełne' : 'Puste'}`
|
statusText: t(locale, 'worker.logicCol', { row: result.index + 1, col: c + 1, state: stateLabel })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,17 +170,18 @@ const handleStep = (playerGrid, solution) => {
|
|||||||
r,
|
r,
|
||||||
c,
|
c,
|
||||||
state: newState,
|
state: newState,
|
||||||
statusText: `Zgadywanie: Wiersz ${r + 1}, Kolumna ${c + 1}`
|
statusText: t(locale, 'worker.guess', { row: r + 1, col: c + 1 })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'done', statusText: 'Koniec!' };
|
return { type: 'done', statusText: t(locale, 'worker.done') };
|
||||||
};
|
};
|
||||||
|
|
||||||
self.onmessage = (event) => {
|
self.onmessage = (event) => {
|
||||||
const { id, playerGrid, solution } = event.data;
|
const { id, playerGrid, solution, locale } = event.data;
|
||||||
const result = handleStep(playerGrid, solution);
|
const resolved = resolveLocale(locale);
|
||||||
|
const result = handleStep(playerGrid, solution, resolved);
|
||||||
self.postMessage({ id, ...result });
|
self.postMessage({ id, ...result });
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user