mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
Ampliar i18n (EN/ES) en labels del dashboard y mensajes de update (#112)
* Expand EN/ES translations for panels and update status texts * Translate remaining dashboard labels and localize country names * Translate Players top action buttons and search placeholder
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { t } from '../i18n';
|
||||||
import { useScoreboardStore } from '../stores/scoreboard';
|
import { useScoreboardStore } from '../stores/scoreboard';
|
||||||
|
|
||||||
const scoreboardStore = useScoreboardStore();
|
const scoreboardStore = useScoreboardStore();
|
||||||
@@ -103,20 +104,20 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="bracket-panel">
|
<div class="bracket-panel">
|
||||||
<div class="text-h4">
|
<div class="text-h4">
|
||||||
Bracket
|
{{ t('bracketTitle') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column q-gutter-md">
|
<div class="column q-gutter-md">
|
||||||
<QSelect
|
<QSelect
|
||||||
v-model="stage"
|
v-model="stage"
|
||||||
label="Stage"
|
:label="t('bracketStage')"
|
||||||
:options="stageOptions"
|
:options="stageOptions"
|
||||||
dense
|
dense
|
||||||
class="bracket-panel__field"
|
class="bracket-panel__field"
|
||||||
/>
|
/>
|
||||||
<QSelect
|
<QSelect
|
||||||
v-model="bracketSide"
|
v-model="bracketSide"
|
||||||
label="Bracket side"
|
:label="t('bracketSide')"
|
||||||
:options="bracketSideOptions"
|
:options="bracketSideOptions"
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
@@ -126,7 +127,7 @@ onMounted(() => {
|
|||||||
<div class="bracket-panel-custom">
|
<div class="bracket-panel-custom">
|
||||||
<QInput
|
<QInput
|
||||||
v-model="customText"
|
v-model="customText"
|
||||||
label="Custom progress"
|
:label="t('bracketCustomProgress')"
|
||||||
dense
|
dense
|
||||||
class="bracket-panel-custom-input bracket-panel__field"
|
class="bracket-panel-custom-input bracket-panel__field"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { t } from '../i18n';
|
||||||
import { useCommentaryStore } from '../stores/commentary';
|
import { useCommentaryStore } from '../stores/commentary';
|
||||||
|
|
||||||
const commentaryStore = useCommentaryStore();
|
const commentaryStore = useCommentaryStore();
|
||||||
@@ -8,7 +9,7 @@ const commentaryStore = useCommentaryStore();
|
|||||||
<div class="commentary-panel">
|
<div class="commentary-panel">
|
||||||
<div class="row items-center q-mb-md">
|
<div class="row items-center q-mb-md">
|
||||||
<div class="text-h4">
|
<div class="text-h4">
|
||||||
Commentary
|
{{ t('commentaryTitle') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ const commentaryStore = useCommentaryStore();
|
|||||||
<div class="commentary-panel__commentator">
|
<div class="commentary-panel__commentator">
|
||||||
<QInput
|
<QInput
|
||||||
v-model="commentaryStore.leftCommentator"
|
v-model="commentaryStore.leftCommentator"
|
||||||
label="Commentator #1"
|
:label="t('commentaryCommentator1')"
|
||||||
dense
|
dense
|
||||||
class="commentary-panel__field"
|
class="commentary-panel__field"
|
||||||
>
|
>
|
||||||
@@ -27,7 +28,7 @@ const commentaryStore = useCommentaryStore();
|
|||||||
|
|
||||||
<QInput
|
<QInput
|
||||||
v-model="commentaryStore.leftCommentatorTwitter"
|
v-model="commentaryStore.leftCommentatorTwitter"
|
||||||
label="@Twitter / Text"
|
:label="t('commentaryTwitterText')"
|
||||||
dense
|
dense
|
||||||
class="commentary-panel__field"
|
class="commentary-panel__field"
|
||||||
/>
|
/>
|
||||||
@@ -45,7 +46,7 @@ const commentaryStore = useCommentaryStore();
|
|||||||
<div class="commentary-panel__commentator">
|
<div class="commentary-panel__commentator">
|
||||||
<QInput
|
<QInput
|
||||||
v-model="commentaryStore.rightCommentator"
|
v-model="commentaryStore.rightCommentator"
|
||||||
label="Commentator #2"
|
:label="t('commentaryCommentator2')"
|
||||||
dense
|
dense
|
||||||
class="commentary-panel__field"
|
class="commentary-panel__field"
|
||||||
>
|
>
|
||||||
@@ -56,7 +57,7 @@ const commentaryStore = useCommentaryStore();
|
|||||||
|
|
||||||
<QInput
|
<QInput
|
||||||
v-model="commentaryStore.rightCommentatorTwitter"
|
v-model="commentaryStore.rightCommentatorTwitter"
|
||||||
label="@Twitter / Text"
|
:label="t('commentaryTwitterText')"
|
||||||
dense
|
dense
|
||||||
class="commentary-panel__field"
|
class="commentary-panel__field"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, watchEffect, type Ref } from 'vue';
|
import { computed, ref, watch, watchEffect, type Ref } from 'vue';
|
||||||
import { countryOptions, getCountryLabel } from '../../../shared/countries';
|
import { getCountryLabel, getCountryOptions } from '../../../shared/countries';
|
||||||
import { getCharactersByGame, getDefaultCharactersByGame } from '../../../shared/fighting-characters';
|
import { getCharactersByGame, getDefaultCharactersByGame } from '../../../shared/fighting-characters';
|
||||||
import type { Schemas } from '../../../types';
|
import type { Schemas } from '../../../types';
|
||||||
import { usePlayersStore } from '../stores/players';
|
import { usePlayersStore } from '../stores/players';
|
||||||
import { useScoreboardStore } from '../stores/scoreboard';
|
import { useScoreboardStore } from '../stores/scoreboard';
|
||||||
|
import { locale, t } from '../i18n';
|
||||||
|
|
||||||
const playersStore = usePlayersStore();
|
const playersStore = usePlayersStore();
|
||||||
const scoreboardStore = useScoreboardStore();
|
const scoreboardStore = useScoreboardStore();
|
||||||
@@ -21,8 +22,9 @@ const rightFocused = ref(false);
|
|||||||
|
|
||||||
const leftCountryInput = ref('');
|
const leftCountryInput = ref('');
|
||||||
const rightCountryInput = ref('');
|
const rightCountryInput = ref('');
|
||||||
const leftCountryOptions = ref(countryOptions);
|
const countryOptions = computed(() => getCountryOptions(locale.value));
|
||||||
const rightCountryOptions = ref(countryOptions);
|
const leftCountryOptions = ref(countryOptions.value);
|
||||||
|
const rightCountryOptions = ref(countryOptions.value);
|
||||||
|
|
||||||
const leftCharacterInput = ref('');
|
const leftCharacterInput = ref('');
|
||||||
const rightCharacterInput = ref('');
|
const rightCharacterInput = ref('');
|
||||||
@@ -81,10 +83,10 @@ const filterCountries = (
|
|||||||
update(() => {
|
update(() => {
|
||||||
const needle = value.toLowerCase().trim();
|
const needle = value.toLowerCase().trim();
|
||||||
if (!needle) {
|
if (!needle) {
|
||||||
target.value = countryOptions;
|
target.value = countryOptions.value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
target.value = countryOptions.filter((country) => country.label.toLowerCase().includes(needle));
|
target.value = countryOptions.value.filter((country) => country.label.toLowerCase().includes(needle));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,7 +135,7 @@ const onGameFilter = (value: string, update: (callback: () => void) => void) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const playerOptions = computed(() => {
|
const playerOptions = computed(() => {
|
||||||
const base = [{ label: '(Sin asignar)', value: '' }];
|
const base = [{ label: t('scoreboardUnassigned'), value: '' }];
|
||||||
const entries = Object.entries(playersStore.players) as [string, Schemas.Players[string]][];
|
const entries = Object.entries(playersStore.players) as [string, Schemas.Players[string]][];
|
||||||
const options = entries.map(([id, player]) => ({
|
const options = entries.map(([id, player]) => ({
|
||||||
value: id,
|
value: id,
|
||||||
@@ -608,7 +610,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => scoreboardStore.scoreboard.leftCountryOverride,
|
() => scoreboardStore.scoreboard.leftCountryOverride,
|
||||||
(value) => {
|
(value) => {
|
||||||
leftCountryInput.value = getCountryLabel(value);
|
leftCountryInput.value = getCountryLabel(value, locale.value);
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -616,7 +618,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => scoreboardStore.scoreboard.rightCountryOverride,
|
() => scoreboardStore.scoreboard.rightCountryOverride,
|
||||||
(value) => {
|
(value) => {
|
||||||
rightCountryInput.value = getCountryLabel(value);
|
rightCountryInput.value = getCountryLabel(value, locale.value);
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -695,6 +697,11 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(countryOptions, (value) => {
|
||||||
|
leftCountryOptions.value = value;
|
||||||
|
rightCountryOptions.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => scoreboardStore.scoreboard.leftCharacter,
|
() => scoreboardStore.scoreboard.leftCharacter,
|
||||||
(value) => {
|
(value) => {
|
||||||
@@ -745,14 +752,14 @@ watch(
|
|||||||
<img
|
<img
|
||||||
v-if="leftPanelImage"
|
v-if="leftPanelImage"
|
||||||
:src="leftPanelImage"
|
:src="leftPanelImage"
|
||||||
:alt="`${leftDisplayName || 'Left'} preview`"
|
:alt="`${leftDisplayName || t('scoreboardLeft')} ${t('scoreboardPreview')}`"
|
||||||
class="scoreboard-preview__image"
|
class="scoreboard-preview__image"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="scoreboard-preview__empty"
|
class="scoreboard-preview__empty"
|
||||||
>
|
>
|
||||||
Left image
|
{{ t('scoreboardLeftImage') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<QSelect
|
<QSelect
|
||||||
@@ -763,7 +770,7 @@ watch(
|
|||||||
option-label="label"
|
option-label="label"
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
label="Character"
|
:label="t('scoreboardLabelCharacter')"
|
||||||
dense
|
dense
|
||||||
use-input
|
use-input
|
||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
@@ -780,7 +787,7 @@ watch(
|
|||||||
v-model="scoreboardStore.scoreboard.leftPlayerId"
|
v-model="scoreboardStore.scoreboard.leftPlayerId"
|
||||||
v-model:input-value="leftInput"
|
v-model:input-value="leftInput"
|
||||||
:options="leftPlayerOptions"
|
:options="leftPlayerOptions"
|
||||||
label="Player"
|
:label="t('scoreboardLabelPlayer')"
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
@@ -809,7 +816,7 @@ watch(
|
|||||||
</QSelect>
|
</QSelect>
|
||||||
<QInput
|
<QInput
|
||||||
v-model="scoreboardStore.scoreboard.leftTeamOverride"
|
v-model="scoreboardStore.scoreboard.leftTeamOverride"
|
||||||
label="Team"
|
:label="t('scoreboardLabelTeam')"
|
||||||
dense
|
dense
|
||||||
class="scoreboard-preview__field"
|
class="scoreboard-preview__field"
|
||||||
>
|
>
|
||||||
@@ -838,7 +845,7 @@ watch(
|
|||||||
hide-selected
|
hide-selected
|
||||||
fill-input
|
fill-input
|
||||||
clearable
|
clearable
|
||||||
label="Country"
|
:label="t('scoreboardLabelCountry')"
|
||||||
dense
|
dense
|
||||||
class="scoreboard-preview__field"
|
class="scoreboard-preview__field"
|
||||||
@filter="onLeftCountryFilter"
|
@filter="onLeftCountryFilter"
|
||||||
@@ -864,7 +871,7 @@ watch(
|
|||||||
v-model="scoreboardStore.scoreboard.game"
|
v-model="scoreboardStore.scoreboard.game"
|
||||||
v-model:input-value="gameInput"
|
v-model:input-value="gameInput"
|
||||||
:options="fightingGameOptions"
|
:options="fightingGameOptions"
|
||||||
label="Game"
|
:label="t('scoreboardLabelGame')"
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
@@ -945,7 +952,7 @@ watch(
|
|||||||
v-model="scoreboardStore.scoreboard.rightPlayerId"
|
v-model="scoreboardStore.scoreboard.rightPlayerId"
|
||||||
v-model:input-value="rightInput"
|
v-model:input-value="rightInput"
|
||||||
:options="rightPlayerOptions"
|
:options="rightPlayerOptions"
|
||||||
label="Player"
|
:label="t('scoreboardLabelPlayer')"
|
||||||
dense
|
dense
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
@@ -974,7 +981,7 @@ watch(
|
|||||||
</QSelect>
|
</QSelect>
|
||||||
<QInput
|
<QInput
|
||||||
v-model="scoreboardStore.scoreboard.rightTeamOverride"
|
v-model="scoreboardStore.scoreboard.rightTeamOverride"
|
||||||
label="Team"
|
:label="t('scoreboardLabelTeam')"
|
||||||
dense
|
dense
|
||||||
class="scoreboard-preview__field"
|
class="scoreboard-preview__field"
|
||||||
>
|
>
|
||||||
@@ -1003,7 +1010,7 @@ watch(
|
|||||||
hide-selected
|
hide-selected
|
||||||
fill-input
|
fill-input
|
||||||
clearable
|
clearable
|
||||||
label="Country"
|
:label="t('scoreboardLabelCountry')"
|
||||||
dense
|
dense
|
||||||
class="scoreboard-preview__field"
|
class="scoreboard-preview__field"
|
||||||
@filter="onRightCountryFilter"
|
@filter="onRightCountryFilter"
|
||||||
@@ -1026,14 +1033,14 @@ watch(
|
|||||||
<img
|
<img
|
||||||
v-if="rightPanelImage"
|
v-if="rightPanelImage"
|
||||||
:src="rightPanelImage"
|
:src="rightPanelImage"
|
||||||
:alt="`${rightDisplayName || 'Right'} preview`"
|
:alt="`${rightDisplayName || t('scoreboardRight')} ${t('scoreboardPreview')}`"
|
||||||
class="scoreboard-preview__image"
|
class="scoreboard-preview__image"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="scoreboard-preview__empty"
|
class="scoreboard-preview__empty"
|
||||||
>
|
>
|
||||||
Right image
|
{{ t('scoreboardRightImage') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<QSelect
|
<QSelect
|
||||||
@@ -1044,7 +1051,7 @@ watch(
|
|||||||
option-label="label"
|
option-label="label"
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
label="Character"
|
:label="t('scoreboardLabelCharacter')"
|
||||||
dense
|
dense
|
||||||
use-input
|
use-input
|
||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export type Locale = 'en' | 'es';
|
||||||
|
|
||||||
|
type Translations = {
|
||||||
|
menuDashboard: string;
|
||||||
|
menuPlayers: string;
|
||||||
|
menuGraphics: string;
|
||||||
|
menuSettings: string;
|
||||||
|
menuAbout: string;
|
||||||
|
settingsTitle: string;
|
||||||
|
settingsDescription: string;
|
||||||
|
settingsLanguageLabel: string;
|
||||||
|
settingsLanguageHint: string;
|
||||||
|
languageEnglish: string;
|
||||||
|
languageSpanish: string;
|
||||||
|
scoreboardUnassigned: string;
|
||||||
|
scoreboardLeft: string;
|
||||||
|
scoreboardRight: string;
|
||||||
|
scoreboardPreview: string;
|
||||||
|
scoreboardLeftImage: string;
|
||||||
|
scoreboardRightImage: string;
|
||||||
|
scoreboardLabelCharacter: string;
|
||||||
|
scoreboardLabelPlayer: string;
|
||||||
|
scoreboardLabelTeam: string;
|
||||||
|
scoreboardLabelCountry: string;
|
||||||
|
scoreboardLabelGame: string;
|
||||||
|
aboutTitle: string;
|
||||||
|
aboutVersion: string;
|
||||||
|
aboutDescription: string;
|
||||||
|
aboutFrameworkNodeCG: string;
|
||||||
|
aboutCollaboratorsTitle: string;
|
||||||
|
aboutUpdateSystemTitle: string;
|
||||||
|
aboutUpdateSystemDescription: string;
|
||||||
|
aboutCheckUpdates: string;
|
||||||
|
aboutLatestRelease: string;
|
||||||
|
aboutPublished: string;
|
||||||
|
aboutUpdateAvailable: string;
|
||||||
|
aboutUpToDate: string;
|
||||||
|
aboutViewRelease: string;
|
||||||
|
aboutElectronNote: string;
|
||||||
|
aboutUnknownReleaseError: string;
|
||||||
|
aboutGitHubStatusError: string;
|
||||||
|
graphicsTitle: string;
|
||||||
|
graphicsDescription: string;
|
||||||
|
graphicsNoConfigured: string;
|
||||||
|
graphicsCopyUrl: string;
|
||||||
|
graphicsDragObs: string;
|
||||||
|
graphicsOverlayPreview: string;
|
||||||
|
graphicsPreviewTitle: string;
|
||||||
|
commentaryTitle: string;
|
||||||
|
commentaryCommentator1: string;
|
||||||
|
commentaryCommentator2: string;
|
||||||
|
commentaryTwitterText: string;
|
||||||
|
bracketTitle: string;
|
||||||
|
bracketStage: string;
|
||||||
|
bracketSide: string;
|
||||||
|
bracketCustomProgress: string;
|
||||||
|
playersLabelTeam: string;
|
||||||
|
playersLabelCountry: string;
|
||||||
|
playersLabelActions: string;
|
||||||
|
playersStartggHelp: string;
|
||||||
|
playersConnectStartgg: string;
|
||||||
|
playersConnected: string;
|
||||||
|
playersUsePersonalApi: string;
|
||||||
|
playersTournament: string;
|
||||||
|
playersImportPlayers: string;
|
||||||
|
playersChallongeHelp: string;
|
||||||
|
playersConnectChallonge: string;
|
||||||
|
playersNewPlayer: string;
|
||||||
|
playersSearchPlaceholder: string;
|
||||||
|
playersImport: string;
|
||||||
|
playersExport: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'scoreko-dev.language';
|
||||||
|
|
||||||
|
const messages: Record<Locale, Translations> = {
|
||||||
|
en: {
|
||||||
|
menuDashboard: 'Dashboard',
|
||||||
|
menuPlayers: 'Players',
|
||||||
|
menuGraphics: 'Graphics',
|
||||||
|
menuSettings: 'Settings',
|
||||||
|
menuAbout: 'About',
|
||||||
|
settingsTitle: 'Settings',
|
||||||
|
settingsDescription: 'Dashboard and bundle configuration.',
|
||||||
|
settingsLanguageLabel: 'Language',
|
||||||
|
settingsLanguageHint: 'Choose the dashboard language.',
|
||||||
|
languageEnglish: 'English',
|
||||||
|
languageSpanish: 'Spanish',
|
||||||
|
scoreboardUnassigned: '(Unassigned)',
|
||||||
|
scoreboardLeft: 'Left',
|
||||||
|
scoreboardRight: 'Right',
|
||||||
|
scoreboardPreview: 'preview',
|
||||||
|
scoreboardLeftImage: 'Left image',
|
||||||
|
scoreboardRightImage: 'Right image',
|
||||||
|
scoreboardLabelCharacter: 'Character',
|
||||||
|
scoreboardLabelPlayer: 'Player',
|
||||||
|
scoreboardLabelTeam: 'Team',
|
||||||
|
scoreboardLabelCountry: 'Country',
|
||||||
|
scoreboardLabelGame: 'Game',
|
||||||
|
aboutTitle: 'About',
|
||||||
|
aboutVersion: 'Version',
|
||||||
|
aboutDescription: 'Dashboard for producing fighting game overlays using NodeCG, Vue, and Quasar.',
|
||||||
|
aboutFrameworkNodeCG: 'Framework NodeCG',
|
||||||
|
aboutCollaboratorsTitle: 'Collaborators and acknowledgments',
|
||||||
|
aboutUpdateSystemTitle: 'Update system (GitHub Releases)',
|
||||||
|
aboutUpdateSystemDescription: 'This check fetches the latest release from the repository and compares it with the current version.',
|
||||||
|
aboutCheckUpdates: 'Check for updates',
|
||||||
|
aboutLatestRelease: 'Latest release',
|
||||||
|
aboutPublished: 'Published',
|
||||||
|
aboutUpdateAvailable: 'A newer version is available.',
|
||||||
|
aboutUpToDate: 'Your version is up to date with the latest release.',
|
||||||
|
aboutViewRelease: 'View release',
|
||||||
|
aboutElectronNote: 'Note for Electron: this panel only implements detection and notification. For real automatic desktop updates, you need to integrate autoUpdater into Electron\'s main process and publish signed artifacts per platform.',
|
||||||
|
aboutUnknownReleaseError: 'Unknown error while checking releases.',
|
||||||
|
aboutGitHubStatusError: 'GitHub responded with status',
|
||||||
|
graphicsTitle: 'Graphics',
|
||||||
|
graphicsDescription: 'Bundle graphics controls and status.',
|
||||||
|
graphicsNoConfigured: 'There are no graphics configured in this bundle.',
|
||||||
|
graphicsCopyUrl: 'Copy URL',
|
||||||
|
graphicsDragObs: 'Drag into OBS',
|
||||||
|
graphicsOverlayPreview: 'Overlay preview (real)',
|
||||||
|
graphicsPreviewTitle: 'Graphic preview',
|
||||||
|
commentaryTitle: 'Commentary',
|
||||||
|
commentaryCommentator1: 'Commentator #1',
|
||||||
|
commentaryCommentator2: 'Commentator #2',
|
||||||
|
commentaryTwitterText: '@Twitter / Text',
|
||||||
|
bracketTitle: 'Bracket',
|
||||||
|
bracketStage: 'Stage',
|
||||||
|
bracketSide: 'Bracket side',
|
||||||
|
bracketCustomProgress: 'Custom progress',
|
||||||
|
playersLabelTeam: 'Team',
|
||||||
|
playersLabelCountry: 'Country',
|
||||||
|
playersLabelActions: 'Actions',
|
||||||
|
playersStartggHelp: 'Connect via OAuth (recommended) or paste your personal token to load tournaments you created or administrate. If you see "Client authentication failed", verify your config uses the Client ID/Secret from a start.gg OAuth App.',
|
||||||
|
playersConnectStartgg: 'Connect with start.gg',
|
||||||
|
playersConnected: 'Connected',
|
||||||
|
playersUsePersonalApi: 'Use personal API',
|
||||||
|
playersTournament: 'Tournament',
|
||||||
|
playersImportPlayers: 'Import players',
|
||||||
|
playersChallongeHelp: 'Connect with OAuth or paste your personal token to load your Challonge tournaments and import participants.',
|
||||||
|
playersConnectChallonge: 'Connect with Challonge',
|
||||||
|
playersNewPlayer: 'New player',
|
||||||
|
playersSearchPlaceholder: 'Search...',
|
||||||
|
playersImport: 'Import',
|
||||||
|
playersExport: 'Export',
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
menuDashboard: 'Panel',
|
||||||
|
menuPlayers: 'Jugadores',
|
||||||
|
menuGraphics: 'Gráficos',
|
||||||
|
menuSettings: 'Configuración',
|
||||||
|
menuAbout: 'Acerca de',
|
||||||
|
settingsTitle: 'Configuración',
|
||||||
|
settingsDescription: 'Configuración del dashboard y del bundle.',
|
||||||
|
settingsLanguageLabel: 'Idioma',
|
||||||
|
settingsLanguageHint: 'Selecciona el idioma del dashboard.',
|
||||||
|
languageEnglish: 'Inglés',
|
||||||
|
languageSpanish: 'Castellano',
|
||||||
|
scoreboardUnassigned: '(Sin asignar)',
|
||||||
|
scoreboardLeft: 'Izquierda',
|
||||||
|
scoreboardRight: 'Derecha',
|
||||||
|
scoreboardPreview: 'vista previa',
|
||||||
|
scoreboardLeftImage: 'Imagen izquierda',
|
||||||
|
scoreboardRightImage: 'Imagen derecha',
|
||||||
|
scoreboardLabelCharacter: 'Personaje',
|
||||||
|
scoreboardLabelPlayer: 'Jugador',
|
||||||
|
scoreboardLabelTeam: 'Equipo',
|
||||||
|
scoreboardLabelCountry: 'País',
|
||||||
|
scoreboardLabelGame: 'Juego',
|
||||||
|
aboutTitle: 'Acerca de',
|
||||||
|
aboutVersion: 'Versión',
|
||||||
|
aboutDescription: 'Dashboard para producir overlays de juegos de lucha usando NodeCG, Vue y Quasar.',
|
||||||
|
aboutFrameworkNodeCG: 'Framework NodeCG',
|
||||||
|
aboutCollaboratorsTitle: 'Colaboradores y agradecimientos',
|
||||||
|
aboutUpdateSystemTitle: 'Sistema de actualizaciones (GitHub Releases)',
|
||||||
|
aboutUpdateSystemDescription: 'Esta comprobación obtiene la última release del repositorio y la compara con la versión actual.',
|
||||||
|
aboutCheckUpdates: 'Buscar actualizaciones',
|
||||||
|
aboutLatestRelease: 'Última release',
|
||||||
|
aboutPublished: 'Publicado',
|
||||||
|
aboutUpdateAvailable: 'Hay una versión más nueva disponible.',
|
||||||
|
aboutUpToDate: 'Tu versión está actualizada con la última release.',
|
||||||
|
aboutViewRelease: 'Ver release',
|
||||||
|
aboutElectronNote: 'Nota para Electron: este panel solo implementa detección y notificación. Para actualizaciones automáticas reales de escritorio, debes integrar autoUpdater en el proceso principal de Electron y publicar artefactos firmados por plataforma.',
|
||||||
|
aboutUnknownReleaseError: 'Error desconocido al consultar releases.',
|
||||||
|
aboutGitHubStatusError: 'GitHub respondió con estado',
|
||||||
|
graphicsTitle: 'Gráficos',
|
||||||
|
graphicsDescription: 'Controles y estado de los gráficos del bundle.',
|
||||||
|
graphicsNoConfigured: 'No hay gráficos configurados en este bundle.',
|
||||||
|
graphicsCopyUrl: 'Copiar URL',
|
||||||
|
graphicsDragObs: 'Arrastrar a OBS',
|
||||||
|
graphicsOverlayPreview: 'Vista previa del overlay (real)',
|
||||||
|
graphicsPreviewTitle: 'Vista previa del gráfico',
|
||||||
|
commentaryTitle: 'Comentario',
|
||||||
|
commentaryCommentator1: 'Comentarista #1',
|
||||||
|
commentaryCommentator2: 'Comentarista #2',
|
||||||
|
commentaryTwitterText: '@Twitter / Texto',
|
||||||
|
bracketTitle: 'Bracket',
|
||||||
|
bracketStage: 'Etapa',
|
||||||
|
bracketSide: 'Lado del bracket',
|
||||||
|
bracketCustomProgress: 'Progreso personalizado',
|
||||||
|
playersLabelTeam: 'Equipo',
|
||||||
|
playersLabelCountry: 'País',
|
||||||
|
playersLabelActions: 'Acciones',
|
||||||
|
playersStartggHelp: 'Conéctate por OAuth (recomendado) o pega tu token personal para cargar torneos que creaste o administras. Si ves "Client authentication failed", revisa que tu configuración use el Client ID/Secret de una app OAuth de start.gg.',
|
||||||
|
playersConnectStartgg: 'Conectar con start.gg',
|
||||||
|
playersConnected: 'Conectado',
|
||||||
|
playersUsePersonalApi: 'Usar API personal',
|
||||||
|
playersTournament: 'Torneo',
|
||||||
|
playersImportPlayers: 'Importar jugadores',
|
||||||
|
playersChallongeHelp: 'Conéctate con OAuth o pega tu token personal para cargar tus torneos de Challonge e importar participantes.',
|
||||||
|
playersConnectChallonge: 'Conectar con Challonge',
|
||||||
|
playersNewPlayer: 'Nuevo jugador',
|
||||||
|
playersSearchPlaceholder: 'Buscar...',
|
||||||
|
playersImport: 'Importar',
|
||||||
|
playersExport: 'Exportar',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeLocale = (value: unknown): Locale => (value === 'es' ? 'es' : 'en');
|
||||||
|
|
||||||
|
const getStoredLocale = (): Locale => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeLocale(localStorage.getItem(STORAGE_KEY));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const locale = ref<Locale>(getStoredLocale());
|
||||||
|
|
||||||
|
export const setLocale = (value: Locale) => {
|
||||||
|
locale.value = normalizeLocale(value);
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(STORAGE_KEY, locale.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const t = (key: keyof Translations): string => messages[locale.value][key];
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const menuItems = [
|
import { computed } from 'vue';
|
||||||
{ label: 'Dashboard', to: '/', icon: 'dashboard' },
|
import { t } from './i18n';
|
||||||
{ label: 'Players', to: '/players', icon: 'groups' },
|
|
||||||
{ label: 'Graphics', to: '/graphics', icon: 'collections' },
|
const menuItems = computed(() => [
|
||||||
{ label: 'Settings', to: '/settings', icon: 'settings' },
|
{ label: t('menuDashboard'), to: '/', icon: 'dashboard' },
|
||||||
{ label: 'About', to: '/about', icon: 'info' },
|
{ label: t('menuPlayers'), to: '/players', icon: 'groups' },
|
||||||
];
|
{ label: t('menuGraphics'), to: '/graphics', icon: 'collections' },
|
||||||
|
{ label: t('menuSettings'), to: '/settings', icon: 'settings' },
|
||||||
|
{ label: t('menuAbout'), to: '/about', icon: 'info' },
|
||||||
|
]);
|
||||||
|
|
||||||
const logoUrl = new URL('./image.png', import.meta.url).href;
|
const logoUrl = new URL('./image.png', import.meta.url).href;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useHead } from '@unhead/vue';
|
import { useHead } from '@unhead/vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { t } from '../i18n';
|
||||||
|
|
||||||
defineOptions({ name: 'AboutView' });
|
defineOptions({ name: 'AboutView' });
|
||||||
|
|
||||||
useHead({ title: 'About' });
|
useHead(() => ({ title: t('aboutTitle') }));
|
||||||
|
|
||||||
type ReleaseResponse = {
|
type ReleaseResponse = {
|
||||||
html_url: string;
|
html_url: string;
|
||||||
@@ -101,13 +102,13 @@ async function checkForUpdates() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`GitHub responded with status ${response.status}.`);
|
throw new Error(`${t('aboutGitHubStatusError')} ${response.status}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
latestRelease.value = await response.json() as ReleaseResponse;
|
latestRelease.value = await response.json() as ReleaseResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
latestRelease.value = null;
|
latestRelease.value = null;
|
||||||
updateError.value = error instanceof Error ? error.message : 'Unknown error while checking releases.';
|
updateError.value = error instanceof Error ? error.message : t('aboutUnknownReleaseError');
|
||||||
} finally {
|
} finally {
|
||||||
checkingUpdates.value = false;
|
checkingUpdates.value = false;
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<QPage class="q-pa-lg">
|
<QPage class="q-pa-lg">
|
||||||
<div class="text-h4 q-mb-md">
|
<div class="text-h4 q-mb-md">
|
||||||
About
|
{{ t('aboutTitle') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-col-gutter-lg">
|
<div class="row q-col-gutter-lg">
|
||||||
@@ -145,7 +146,7 @@ onMounted(() => {
|
|||||||
{{ appName }}
|
{{ appName }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption text-grey-7">
|
<div class="text-caption text-grey-7">
|
||||||
Version {{ currentVersion }}
|
{{ t('aboutVersion') }} {{ currentVersion }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
@@ -154,7 +155,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<p class="q-mb-sm">
|
<p class="q-mb-sm">
|
||||||
Dashboard for producing fighting game overlays using NodeCG, Vue, and Quasar.
|
{{ t('aboutDescription') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="column q-gutter-sm">
|
<div class="column q-gutter-sm">
|
||||||
<QBtn
|
<QBtn
|
||||||
@@ -162,7 +163,7 @@ onMounted(() => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
icon="open_in_new"
|
icon="open_in_new"
|
||||||
label="Framework NodeCG"
|
:label="t('aboutFrameworkNodeCG')"
|
||||||
color="primary"
|
color="primary"
|
||||||
flat
|
flat
|
||||||
no-caps
|
no-caps
|
||||||
@@ -175,7 +176,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="text-subtitle2 q-mb-sm">
|
<div class="text-subtitle2 q-mb-sm">
|
||||||
Collaborators and acknowledgments
|
{{ t('aboutCollaboratorsTitle') }}
|
||||||
</div>
|
</div>
|
||||||
<QList dense>
|
<QList dense>
|
||||||
<QItem
|
<QItem
|
||||||
@@ -205,10 +206,10 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
Update system (GitHub Releases)
|
{{ t('aboutUpdateSystemTitle') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-body2 text-grey-7 q-mt-xs">
|
<div class="text-body2 text-grey-7 q-mt-xs">
|
||||||
This check fetches the latest release from the repository and compares it with the current version.
|
{{ t('aboutUpdateSystemDescription') }}
|
||||||
</div>
|
</div>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
|
|
||||||
@@ -216,7 +217,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<QCardSection class="q-gutter-md">
|
<QCardSection class="q-gutter-md">
|
||||||
<QBtn
|
<QBtn
|
||||||
label="Check for updates"
|
:label="t('aboutCheckUpdates')"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="sync"
|
icon="sync"
|
||||||
:loading="checkingUpdates"
|
:loading="checkingUpdates"
|
||||||
@@ -236,19 +237,19 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div class="text-subtitle2">
|
<div class="text-subtitle2">
|
||||||
Latest release: {{ releaseLabel }}
|
{{ t('aboutLatestRelease') }}: {{ releaseLabel }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption text-grey-7">
|
<div class="text-caption text-grey-7">
|
||||||
Published: {{ new Date(latestRelease.published_at).toLocaleString() }}
|
{{ t('aboutPublished') }}: {{ new Date(latestRelease.published_at).toLocaleString() }}
|
||||||
</div>
|
</div>
|
||||||
<div class="q-mt-sm">
|
<div class="q-mt-sm">
|
||||||
{{ hasUpdate ? 'A newer version is available.' : 'Your version is up to date with the latest release.' }}
|
{{ hasUpdate ? t('aboutUpdateAvailable') : t('aboutUpToDate') }}
|
||||||
</div>
|
</div>
|
||||||
<template #action>
|
<template #action>
|
||||||
<QBtn
|
<QBtn
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
label="View release"
|
:label="t('aboutViewRelease')"
|
||||||
:href="releaseUrl"
|
:href="releaseUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -269,9 +270,7 @@ onMounted(() => {
|
|||||||
rounded
|
rounded
|
||||||
class="bg-blue-1 text-blue-10"
|
class="bg-blue-1 text-blue-10"
|
||||||
>
|
>
|
||||||
Note for Electron: this panel only implements <strong>detection and notification</strong>. For real automatic
|
{{ t('aboutElectronNote') }}
|
||||||
desktop updates, you need to integrate `autoUpdater` into Electron's main process
|
|
||||||
and publish signed artifacts per platform.
|
|
||||||
</QBanner>
|
</QBanner>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
</QCard>
|
</QCard>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useHead } from '@unhead/vue';
|
import { useHead } from '@unhead/vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { t } from '../i18n';
|
||||||
|
|
||||||
defineOptions({ name: 'GraphicsView' });
|
defineOptions({ name: 'GraphicsView' });
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ type GraphicConfig = {
|
|||||||
|
|
||||||
type PreviewKind = 'scoreboard' | 'commentary' | null;
|
type PreviewKind = 'scoreboard' | 'commentary' | null;
|
||||||
|
|
||||||
useHead({ title: 'Graphics' });
|
useHead(() => ({ title: t('graphicsTitle') }));
|
||||||
|
|
||||||
const graphics = computed<GraphicConfig[]>(
|
const graphics = computed<GraphicConfig[]>(
|
||||||
() => bundlePackage.nodecg?.graphics ?? [],
|
() => bundlePackage.nodecg?.graphics ?? [],
|
||||||
@@ -84,17 +85,17 @@ const onDragStart = (event: DragEvent, graphic: GraphicConfig) => {
|
|||||||
<template>
|
<template>
|
||||||
<QPage class="q-pa-lg">
|
<QPage class="q-pa-lg">
|
||||||
<div class="text-h4 q-mb-md">
|
<div class="text-h4 q-mb-md">
|
||||||
Graphics
|
{{ t('graphicsTitle') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-body1 q-mb-lg">
|
<div class="text-body1 q-mb-lg">
|
||||||
Bundle graphics controls and status.
|
{{ t('graphicsDescription') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="graphics.length === 0"
|
v-if="graphics.length === 0"
|
||||||
class="text-body2 text-grey-5"
|
class="text-body2 text-grey-5"
|
||||||
>
|
>
|
||||||
There are no graphics configured in this bundle.
|
{{ t('graphicsNoConfigured') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
@@ -131,13 +132,13 @@ const onDragStart = (event: DragEvent, graphic: GraphicConfig) => {
|
|||||||
<QBtn
|
<QBtn
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="content_copy"
|
icon="content_copy"
|
||||||
label="Copy URL"
|
:label="t('graphicsCopyUrl')"
|
||||||
@click="copyUrl(graphic)"
|
@click="copyUrl(graphic)"
|
||||||
/>
|
/>
|
||||||
<QBtn
|
<QBtn
|
||||||
color="secondary"
|
color="secondary"
|
||||||
icon="open_with"
|
icon="open_with"
|
||||||
label="Drag into OBS"
|
:label="t('graphicsDragObs')"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart($event, graphic)"
|
@dragstart="onDragStart($event, graphic)"
|
||||||
/>
|
/>
|
||||||
@@ -148,14 +149,14 @@ const onDragStart = (event: DragEvent, graphic: GraphicConfig) => {
|
|||||||
class="graphics-preview q-mt-md"
|
class="graphics-preview q-mt-md"
|
||||||
>
|
>
|
||||||
<div class="graphics-preview__label text-caption text-grey-5 q-mb-sm">
|
<div class="graphics-preview__label text-caption text-grey-5 q-mb-sm">
|
||||||
Overlay preview (real)
|
{{ t('graphicsOverlayPreview') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="graphics-preview__frame-wrap">
|
<div class="graphics-preview__frame-wrap">
|
||||||
<iframe
|
<iframe
|
||||||
class="graphics-preview__frame"
|
class="graphics-preview__frame"
|
||||||
:src="buildGraphicUrl(graphic)"
|
:src="buildGraphicUrl(graphic)"
|
||||||
title="Graphic preview"
|
:title="t('graphicsPreviewTitle')"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ defineOptions({ name: 'PlayersView' });
|
|||||||
|
|
||||||
import type { QTableColumn } from 'quasar';
|
import type { QTableColumn } from 'quasar';
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { countryOptions, getCountryLabel } from '../../../shared/countries';
|
import { getCountryLabel, getCountryOptions } from '../../../shared/countries';
|
||||||
import type { Schemas } from '../../../types';
|
import type { Schemas } from '../../../types';
|
||||||
|
import { locale, t } from '../i18n';
|
||||||
import { usePlayersStore } from '../stores/players';
|
import { usePlayersStore } from '../stores/players';
|
||||||
|
|
||||||
useHead({ title: 'Players' });
|
useHead(() => ({ title: t('menuPlayers') }));
|
||||||
|
|
||||||
type PlayersMap = Schemas.Players;
|
type PlayersMap = Schemas.Players;
|
||||||
type Player = PlayersMap[string];
|
type Player = PlayersMap[string];
|
||||||
@@ -74,39 +75,44 @@ const emptyPlayer: Player = {
|
|||||||
|
|
||||||
const form = reactive<Player>({ ...emptyPlayer });
|
const form = reactive<Player>({ ...emptyPlayer });
|
||||||
|
|
||||||
const columns: QTableColumn<PlayerRow>[] = [
|
const columns = computed<QTableColumn<PlayerRow>[]>(() => [
|
||||||
{ name: 'gamertag', label: 'Gamertag', field: 'gamertag', sortable: true, align: 'left' },
|
{ name: 'gamertag', label: 'Gamertag', field: 'gamertag', sortable: true, align: 'left' },
|
||||||
{ name: 'team', label: 'Team', field: 'team', sortable: true, align: 'left' },
|
{ name: 'team', label: t('playersLabelTeam'), field: 'team', sortable: true, align: 'left' },
|
||||||
{
|
{
|
||||||
name: 'country',
|
name: 'country',
|
||||||
label: 'Country',
|
label: t('playersLabelCountry'),
|
||||||
field: (row) => getCountryLabel(row.country),
|
field: (row) => getCountryLabel(row.country, locale.value),
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
{ name: 'twitter', label: 'Twitter', field: 'twitter', sortable: true, align: 'left' },
|
{ name: 'twitter', label: 'Twitter', field: 'twitter', sortable: true, align: 'left' },
|
||||||
{ name: 'actions', label: 'Actions', field: (row) => row.id, sortable: false, align: 'right' },
|
{ name: 'actions', label: t('playersLabelActions'), field: (row) => row.id, sortable: false, align: 'right' },
|
||||||
];
|
]);
|
||||||
|
|
||||||
const filteredCountryOptions = ref(countryOptions);
|
const countryOptions = computed(() => getCountryOptions(locale.value));
|
||||||
|
const filteredCountryOptions = ref(countryOptions.value);
|
||||||
const countryInput = ref('');
|
const countryInput = ref('');
|
||||||
const filterCountries = (value: string, update: (callback: () => void) => void) => {
|
const filterCountries = (value: string, update: (callback: () => void) => void) => {
|
||||||
update(() => {
|
update(() => {
|
||||||
const needle = value.toLowerCase().trim();
|
const needle = value.toLowerCase().trim();
|
||||||
if (!needle) {
|
if (!needle) {
|
||||||
filteredCountryOptions.value = countryOptions;
|
filteredCountryOptions.value = countryOptions.value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
filteredCountryOptions.value = countryOptions.filter((country) =>
|
filteredCountryOptions.value = countryOptions.value.filter((country) =>
|
||||||
country.label.toLowerCase().includes(needle),
|
country.label.toLowerCase().includes(needle),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(countryOptions, (value) => {
|
||||||
|
filteredCountryOptions.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => form.country,
|
() => form.country,
|
||||||
(value) => {
|
(value) => {
|
||||||
countryInput.value = getCountryLabel(value);
|
countryInput.value = getCountryLabel(value, locale.value);
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -517,7 +523,7 @@ const selectedChallongeTournamentOption = computed(() =>
|
|||||||
const canImportSelectedChallongeTournament = computed(() => Boolean(selectedChallongeTournamentOption.value));
|
const canImportSelectedChallongeTournament = computed(() => Boolean(selectedChallongeTournamentOption.value));
|
||||||
const hasChallongeTokenConfigured = computed(() => Boolean(challongeToken.value.trim()));
|
const hasChallongeTokenConfigured = computed(() => Boolean(challongeToken.value.trim()));
|
||||||
|
|
||||||
const challongeConnectionLabel = computed(() => (hasValidatedChallongeToken.value ? 'Connected' : 'Token set'));
|
const challongeConnectionLabel = computed(() => (hasValidatedChallongeToken.value ? t('playersConnected') : 'Token set'));
|
||||||
|
|
||||||
const filterChallongeTournaments = (value: string, update: (callback: () => void) => void) => {
|
const filterChallongeTournaments = (value: string, update: (callback: () => void) => void) => {
|
||||||
update(() => {
|
update(() => {
|
||||||
@@ -805,13 +811,13 @@ onBeforeUnmount(() => {
|
|||||||
<QPage class="q-pa-lg players-page">
|
<QPage class="q-pa-lg players-page">
|
||||||
<div class="row items-center q-mb-md">
|
<div class="row items-center q-mb-md">
|
||||||
<div class="text-h4">
|
<div class="text-h4">
|
||||||
Players
|
{{ t('menuPlayers') }}
|
||||||
</div>
|
</div>
|
||||||
<QSpace />
|
<QSpace />
|
||||||
<QBtn
|
<QBtn
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="add"
|
icon="add"
|
||||||
label="New player"
|
:label="t('playersNewPlayer')"
|
||||||
class="q-ml-sm"
|
class="q-ml-sm"
|
||||||
@click="openCreateDialog"
|
@click="openCreateDialog"
|
||||||
/>
|
/>
|
||||||
@@ -823,7 +829,7 @@ onBeforeUnmount(() => {
|
|||||||
<QInput
|
<QInput
|
||||||
v-model="filter"
|
v-model="filter"
|
||||||
dense
|
dense
|
||||||
placeholder="Search..."
|
:placeholder="t('playersSearchPlaceholder')"
|
||||||
class="players-search players-underlined-field"
|
class="players-search players-underlined-field"
|
||||||
clearable
|
clearable
|
||||||
>
|
>
|
||||||
@@ -835,14 +841,14 @@ onBeforeUnmount(() => {
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
outline
|
outline
|
||||||
icon="file_upload"
|
icon="file_upload"
|
||||||
label="Import"
|
:label="t('playersImport')"
|
||||||
@click="triggerImport"
|
@click="triggerImport"
|
||||||
/>
|
/>
|
||||||
<QBtn
|
<QBtn
|
||||||
color="secondary"
|
color="secondary"
|
||||||
outline
|
outline
|
||||||
icon="file_download"
|
icon="file_download"
|
||||||
label="Export"
|
:label="t('playersExport')"
|
||||||
@click="exportPlayers"
|
@click="exportPlayers"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -900,10 +906,10 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<path d="M6 0A5.999 5.999 0 00.002 6v5.252a.75.75 0 00.75.748H5.25a.748.748 0 00.75-.747V6.749C6 6.334 6.336 6 6.748 6h16.497a.748.748 0 00.749-.748V.749A.743.743 0 0023.247 0zm12.75 12a.748.748 0 00-.75.75v4.5a.748.748 0 01-.747.748H.753a.754.754 0 00-.75.751v4.5a.75.75 0 00.75.751H18a5.999 5.999 0 005.999-6v-5.25a.75.75 0 00-.75-.75z" />
|
<path d="M6 0A5.999 5.999 0 00.002 6v5.252a.75.75 0 00.75.748H5.25a.748.748 0 00.75-.747V6.749C6 6.334 6.336 6 6.748 6h16.497a.748.748 0 00.749-.748V.749A.743.743 0 0023.247 0zm12.75 12a.748.748 0 00-.75.75v4.5a.748.748 0 01-.747.748H.753a.754.754 0 00-.75.751v4.5a.75.75 0 00.75.751H18a5.999 5.999 0 005.999-6v-5.25a.75.75 0 00-.75-.75z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>StartGG</span>
|
<span>start.gg</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption q-mb-md">
|
<div class="text-caption q-mb-md">
|
||||||
Connect via OAuth (recommended) or paste your personal token to load tournaments you created or administrate. If you see "Client authentication failed", verify your config uses the Client ID/Secret from a start.gg OAuth App.
|
{{ t('playersStartggHelp') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-sm items-center">
|
<div class="row q-col-gutter-sm items-center">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@@ -911,7 +917,7 @@ onBeforeUnmount(() => {
|
|||||||
v-if="!hasStartGGTokenConfigured"
|
v-if="!hasStartGGTokenConfigured"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="login"
|
icon="login"
|
||||||
label="Connect with start.gg"
|
:label="t('playersConnectStartgg')"
|
||||||
:loading="oauthLoading"
|
:loading="oauthLoading"
|
||||||
@click="connectWithStartGGOAuth"
|
@click="connectWithStartGGOAuth"
|
||||||
/>
|
/>
|
||||||
@@ -920,7 +926,7 @@ onBeforeUnmount(() => {
|
|||||||
outline
|
outline
|
||||||
color="positive"
|
color="positive"
|
||||||
icon="check_circle"
|
icon="check_circle"
|
||||||
label="Connected"
|
:label="t('playersConnected')"
|
||||||
class="startgg-connected-btn"
|
class="startgg-connected-btn"
|
||||||
@click="openManualTokenDialog"
|
@click="openManualTokenDialog"
|
||||||
/>
|
/>
|
||||||
@@ -930,7 +936,7 @@ onBeforeUnmount(() => {
|
|||||||
outline
|
outline
|
||||||
color="white"
|
color="white"
|
||||||
icon="vpn_key"
|
icon="vpn_key"
|
||||||
label="Use personal API"
|
:label="t('playersUsePersonalApi')"
|
||||||
@click="openManualTokenDialog"
|
@click="openManualTokenDialog"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -967,7 +973,7 @@ onBeforeUnmount(() => {
|
|||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
clearable
|
clearable
|
||||||
dense
|
dense
|
||||||
label="Tournament"
|
:label="t('playersTournament')"
|
||||||
class="players-underlined-field"
|
class="players-underlined-field"
|
||||||
@filter="filterTournaments"
|
@filter="filterTournaments"
|
||||||
>
|
>
|
||||||
@@ -992,10 +998,10 @@ onBeforeUnmount(() => {
|
|||||||
unelevated
|
unelevated
|
||||||
round
|
round
|
||||||
icon="person_add"
|
icon="person_add"
|
||||||
aria-label="Import players"
|
:aria-label="t('playersImportPlayers')"
|
||||||
@click="openSelectedTournamentImportDialog"
|
@click="openSelectedTournamentImportDialog"
|
||||||
>
|
>
|
||||||
<QTooltip>Import players</QTooltip>
|
<QTooltip>{{ t('playersImportPlayers') }}</QTooltip>
|
||||||
</QBtn>
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1015,7 +1021,7 @@ onBeforeUnmount(() => {
|
|||||||
<span>Challonge</span>
|
<span>Challonge</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption q-mb-md">
|
<div class="text-caption q-mb-md">
|
||||||
Connect with OAuth or paste your personal token to load your Challonge tournaments and import participants.
|
{{ t('playersChallongeHelp') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-sm items-center">
|
<div class="row q-col-gutter-sm items-center">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@@ -1023,7 +1029,7 @@ onBeforeUnmount(() => {
|
|||||||
v-if="!hasChallongeTokenConfigured"
|
v-if="!hasChallongeTokenConfigured"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="login"
|
icon="login"
|
||||||
label="Connect with Challonge"
|
:label="t('playersConnectChallonge')"
|
||||||
:loading="challongeOauthLoading"
|
:loading="challongeOauthLoading"
|
||||||
@click="connectWithChallongeOAuth"
|
@click="connectWithChallongeOAuth"
|
||||||
/>
|
/>
|
||||||
@@ -1041,7 +1047,7 @@ onBeforeUnmount(() => {
|
|||||||
outline
|
outline
|
||||||
color="white"
|
color="white"
|
||||||
icon="vpn_key"
|
icon="vpn_key"
|
||||||
label="Use personal API"
|
:label="t('playersUsePersonalApi')"
|
||||||
@click="openChallongeManualTokenDialog"
|
@click="openChallongeManualTokenDialog"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1078,7 +1084,7 @@ onBeforeUnmount(() => {
|
|||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
clearable
|
clearable
|
||||||
dense
|
dense
|
||||||
label="Tournament"
|
:label="t('playersTournament')"
|
||||||
class="players-underlined-field"
|
class="players-underlined-field"
|
||||||
@filter="filterChallongeTournaments"
|
@filter="filterChallongeTournaments"
|
||||||
>
|
>
|
||||||
@@ -1103,10 +1109,10 @@ onBeforeUnmount(() => {
|
|||||||
unelevated
|
unelevated
|
||||||
round
|
round
|
||||||
icon="person_add"
|
icon="person_add"
|
||||||
aria-label="Import players"
|
:aria-label="t('playersImportPlayers')"
|
||||||
@click="openSelectedChallongeTournamentImportDialog"
|
@click="openSelectedChallongeTournamentImportDialog"
|
||||||
>
|
>
|
||||||
<QTooltip>Import players</QTooltip>
|
<QTooltip>{{ t('playersImportPlayers') }}</QTooltip>
|
||||||
</QBtn>
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1187,7 +1193,7 @@ onBeforeUnmount(() => {
|
|||||||
v-model="selectedStartGGPlayerIds"
|
v-model="selectedStartGGPlayerIds"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:options="startGGPlayers.map((player) => ({
|
:options="startGGPlayers.map((player) => ({
|
||||||
label: `${player.gamertag}${player.team ? ` (${player.team})` : ''}${player.country ? ` - ${getCountryLabel(player.country)}` : ''}`,
|
label: `${player.gamertag}${player.team ? ` (${player.team})` : ''}${player.country ? ` - ${getCountryLabel(player.country, locale)}` : ''}`,
|
||||||
value: player.id,
|
value: player.id,
|
||||||
}))"
|
}))"
|
||||||
/>
|
/>
|
||||||
@@ -1303,7 +1309,7 @@ onBeforeUnmount(() => {
|
|||||||
<QCard class="players-dialog">
|
<QCard class="players-dialog">
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
{{ editingId ? 'Edit player' : 'New player' }}
|
{{ editingId ? 'Edit player' : t('playersNewPlayer') }}
|
||||||
</div>
|
</div>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
<QSeparator />
|
<QSeparator />
|
||||||
@@ -1341,7 +1347,7 @@ onBeforeUnmount(() => {
|
|||||||
hide-selected
|
hide-selected
|
||||||
fill-input
|
fill-input
|
||||||
clearable
|
clearable
|
||||||
label="Country"
|
:label="t('playersLabelCountry')"
|
||||||
dense
|
dense
|
||||||
class="players-underlined-field"
|
class="players-underlined-field"
|
||||||
@filter="filterCountries"
|
@filter="filterCountries"
|
||||||
@@ -1350,7 +1356,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<QInput
|
<QInput
|
||||||
v-model="form.team"
|
v-model="form.team"
|
||||||
label="Team"
|
:label="t('playersLabelTeam')"
|
||||||
dense
|
dense
|
||||||
class="players-underlined-field"
|
class="players-underlined-field"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,18 +1,54 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
import { useHead } from '@unhead/vue';
|
import { useHead } from '@unhead/vue';
|
||||||
|
import type { Locale } from '../i18n';
|
||||||
|
import { locale, setLocale, t } from '../i18n';
|
||||||
|
|
||||||
defineOptions({ name: 'SettingsView' });
|
defineOptions({ name: 'SettingsView' });
|
||||||
|
|
||||||
useHead({ title: 'Settings' });
|
const languageOptions = computed(() => [
|
||||||
|
{ label: t('languageSpanish'), value: 'es' as const },
|
||||||
|
{ label: t('languageEnglish'), value: 'en' as const },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedLanguage = computed<Locale>({
|
||||||
|
get: () => locale.value,
|
||||||
|
set: (value) => {
|
||||||
|
setLocale(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useHead(() => ({ title: t('settingsTitle') }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<QPage class="q-pa-lg">
|
<QPage class="q-pa-lg">
|
||||||
<div class="text-h4 q-mb-md">
|
<div class="text-h4 q-mb-md">
|
||||||
Settings
|
{{ t('settingsTitle') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-body1">
|
<div class="text-body1 q-mb-lg">
|
||||||
Dashboard and bundle configuration.
|
{{ t('settingsDescription') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<QCard
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
class="q-pa-md"
|
||||||
|
style="max-width: 360px;"
|
||||||
|
>
|
||||||
|
<QCardSection class="q-pa-none">
|
||||||
|
<QSelect
|
||||||
|
v-model="selectedLanguage"
|
||||||
|
outlined
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
:label="t('settingsLanguageLabel')"
|
||||||
|
:options="languageOptions"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-grey-5 q-mt-sm">
|
||||||
|
{{ t('settingsLanguageHint') }}
|
||||||
|
</div>
|
||||||
|
</QCardSection>
|
||||||
|
</QCard>
|
||||||
</QPage>
|
</QPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+32
-10
@@ -7,18 +7,38 @@ export interface CountryOption {
|
|||||||
|
|
||||||
const baseCountries = getData();
|
const baseCountries = getData();
|
||||||
|
|
||||||
export const countryOptions: CountryOption[] = baseCountries
|
const optionsCache = new Map<string, CountryOption[]>();
|
||||||
.map((country: CountryRecord) => ({
|
|
||||||
value: country.code,
|
const getDisplayNames = (locale: string) => {
|
||||||
label: country.name,
|
try {
|
||||||
}))
|
return new Intl.DisplayNames([locale], { type: 'region' });
|
||||||
.sort((a: CountryOption, b: CountryOption) => a.label.localeCompare(b.label));
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCountryOptions = (locale = 'en'): CountryOption[] => {
|
||||||
|
if (optionsCache.has(locale)) {
|
||||||
|
return optionsCache.get(locale)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayNames = getDisplayNames(locale);
|
||||||
|
const options = baseCountries
|
||||||
|
.map((country: CountryRecord) => ({
|
||||||
|
value: country.code,
|
||||||
|
label: displayNames?.of(country.code) ?? country.name,
|
||||||
|
}))
|
||||||
|
.sort((a: CountryOption, b: CountryOption) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
|
optionsCache.set(locale, options);
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
const countryByCode = new Map(
|
const countryByCode = new Map(
|
||||||
countryOptions.map((country) => [country.value.toUpperCase(), country.label]),
|
baseCountries.map((country) => [country.code.toUpperCase(), country.name]),
|
||||||
);
|
);
|
||||||
const countryByName = new Map(
|
const countryByName = new Map(
|
||||||
countryOptions.map((country) => [country.label.toLowerCase(), country.value]),
|
baseCountries.map((country) => [country.name.toLowerCase(), country.code]),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const resolveCountryCode = (value?: string) => {
|
export const resolveCountryCode = (value?: string) => {
|
||||||
@@ -37,7 +57,7 @@ export const resolveCountryCode = (value?: string) => {
|
|||||||
return byName ?? '';
|
return byName ?? '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCountryLabel = (value?: string) => {
|
export const getCountryLabel = (value?: string, locale = 'en') => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -45,5 +65,7 @@ export const getCountryLabel = (value?: string) => {
|
|||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return countryByCode.get(resolved) ?? value;
|
|
||||||
|
const match = getCountryOptions(locale).find((country) => country.value === resolved);
|
||||||
|
return match?.label ?? countryByCode.get(resolved) ?? value;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user