mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-06 00:56:58 +00:00
feat: add internationalization and spanish locale
This commit is contained in:
46
app/components/LanguageSwitcher.vue
Normal file
46
app/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '~/components/ui/select'
|
||||
import { Languages } from 'lucide-vue-next'
|
||||
|
||||
const { locale, locales, setLocale } = useI18n()
|
||||
const { t } = useI18n()
|
||||
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
|
||||
const availableLocales = computed(() => locales.value)
|
||||
|
||||
const currentLocale = computed({
|
||||
get: () => locale.value,
|
||||
set: (value) => {
|
||||
const path = switchLocalePath(value)
|
||||
navigateTo(path)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select v-model="currentLocale">
|
||||
<SelectTrigger class="w-[160px]">
|
||||
<Languages class="mr-2 h-4 w-4" />
|
||||
<SelectValue :placeholder="t('selectLanguage')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="loc in availableLocales"
|
||||
:key="loc.code"
|
||||
:value="loc.code"
|
||||
>
|
||||
{{ loc.name }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</template>
|
||||
@@ -2,9 +2,9 @@
|
||||
<Dialog :open="open" @update:open="$emit('update:open', $event)">
|
||||
<DialogContent class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Preset</DialogTitle>
|
||||
<DialogTitle>{{ t('editPreset') }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Make changes to your preset here. Click save when you're done.
|
||||
{{ t('editPresetDescription') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<PresetForm
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import PresetForm from "~/components/app/PresetForm.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
presetUser: any;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<FieldGroup>
|
||||
<form.Field v-slot="{ field }" name="name">
|
||||
<Field :data-invalid="isInvalid(field)">
|
||||
<FieldLabel :for="`${formId}-name`"> Preset name </FieldLabel>
|
||||
<FieldLabel :for="`${formId}-name`"> {{ t('presetName') }} </FieldLabel>
|
||||
<Input
|
||||
:id="`${formId}-name`"
|
||||
:name="field.name"
|
||||
@@ -24,7 +24,7 @@
|
||||
</FieldGroup>
|
||||
<form.Field v-slot="{ field }" name="iceServers">
|
||||
<Field :data-invalid="isInvalid(field)">
|
||||
<FieldLabel>Ice Servers (JSON)</FieldLabel>
|
||||
<FieldLabel>{{ t('iceServersJson') }}</FieldLabel>
|
||||
<div
|
||||
class="h-96 w-full border rounded-md overflow-hidden focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px] transition"
|
||||
>
|
||||
@@ -47,10 +47,10 @@
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldLabel :for="`${formId}-default`">
|
||||
Set as default preset
|
||||
{{ t('setAsDefaultPreset') }}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
This preset will be selected by default on the preset selector.
|
||||
{{ t('setAsDefaultPresetDescription') }}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
@@ -62,7 +62,7 @@
|
||||
</Field>
|
||||
</form.Field>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="submit" :form="formId">Save</Button>
|
||||
<Button type="submit" :form="formId">{{ t('save') }}</Button>
|
||||
</Field>
|
||||
</form>
|
||||
</div>
|
||||
@@ -84,6 +84,8 @@ import { Input } from "~/components/ui/input";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { schema } from "~/lib/schema/new-preset";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
initialValues?: {
|
||||
name: string;
|
||||
@@ -153,14 +155,14 @@ const form = useForm({
|
||||
if (request.success) {
|
||||
toast.success(
|
||||
props.isEdit
|
||||
? "Preset updated successfully!"
|
||||
: "Preset created successfully!",
|
||||
? t('presetUpdatedSuccessfully')
|
||||
: t('presetCreatedSuccessfully'),
|
||||
);
|
||||
emit("success");
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
props.isEdit ? "Failed to update preset." : "Failed to create preset.",
|
||||
props.isEdit ? t('failedToUpdatePreset') : t('failedToCreatePreset'),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Plus } from "lucide-vue-next";
|
||||
import type { ApiResponse, PresetUser } from "~/lib/types/PresetGetResponse";
|
||||
import { useStreamerStore } from "~/state/streamer";
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const selectedValue = ref("");
|
||||
const presets = ref<PresetUser[]>([]);
|
||||
@@ -70,7 +71,7 @@ watch(selectedValue, (newValue) => {
|
||||
<Select v-model="selectedValue" :disabled="loading">
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue
|
||||
:placeholder="loading ? 'Loading presets...' : 'Select a preset'"
|
||||
:placeholder="loading ? t('loadingPresets') : t('selectAPreset')"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -78,7 +79,7 @@ watch(selectedValue, (newValue) => {
|
||||
v-if="presets.length === 0 && !loading"
|
||||
class="px-2 py-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
No presets available
|
||||
{{ t('noPresetsAvailable') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loading">
|
||||
@@ -90,7 +91,7 @@ watch(selectedValue, (newValue) => {
|
||||
<span
|
||||
v-if="preset.isDefault"
|
||||
class="ml-2 text-xs text-muted-foreground"
|
||||
>(default)</span
|
||||
>({{ t('default') }})</span
|
||||
>
|
||||
</SelectItem>
|
||||
</div>
|
||||
@@ -100,7 +101,7 @@ watch(selectedValue, (newValue) => {
|
||||
<SelectItem value="create-new">
|
||||
<div class="font-bold flex gap-2 items-center">
|
||||
<Plus class="size-4" />
|
||||
Create New Preset
|
||||
{{ t('createNewPreset') }}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import SignInDialog from "~/components/app/SignInDialog.vue";
|
||||
import ThemeDropdown from "~/components/ui/ThemeDropdown.vue";
|
||||
import LanguageSwitcher from "~/components/LanguageSwitcher.vue";
|
||||
import "vue-sonner/style.css";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -18,14 +21,14 @@ import { Toaster } from "@/components/ui/sonner";
|
||||
class="text-sm font-medium hover:text-primary transition-colors"
|
||||
active-class="text-primary"
|
||||
>
|
||||
Home
|
||||
{{ t('home') }}
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/stream"
|
||||
class="text-sm font-medium hover:text-primary transition-colors"
|
||||
active-class="text-primary"
|
||||
>
|
||||
Stream
|
||||
{{ t('stream') }}
|
||||
</NuxtLink>
|
||||
<ClientOnly>
|
||||
<SignedIn>
|
||||
@@ -34,13 +37,14 @@ import { Toaster } from "@/components/ui/sonner";
|
||||
class="text-sm font-medium hover:text-primary transition-colors"
|
||||
active-class="text-primary"
|
||||
>
|
||||
Presets
|
||||
{{ t('presets') }}
|
||||
</NuxtLink>
|
||||
</SignedIn>
|
||||
</ClientOnly>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<LanguageSwitcher />
|
||||
<ThemeDropdown />
|
||||
<ClientOnly>
|
||||
<SignedOut>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col md:flex-row items-center justify-center gap-6 mt-10 px-4 min-h-[80vh]"
|
||||
class="flex flex-col md:flex-row items-center justify-center gap-6 md:gap-20 mt-10 px-4 min-h-[80vh]"
|
||||
>
|
||||
<div
|
||||
v-if="!isConnected"
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-4xl font-bold tracking-tight">helium</h1>
|
||||
<p class="text-muted-foreground text-lg">
|
||||
effortless screensharing powered by webrtc
|
||||
{{ $t("effortlessScreensharing") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<NuxtLink to="/stream">
|
||||
<Button variant="link" class="text-muted-foreground hover:text-primary">
|
||||
host instead?
|
||||
{{ $t("hostInstead") }}
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
@@ -36,9 +36,11 @@
|
||||
class="absolute inset-0 flex items-center justify-center z-10 p-4 text-center"
|
||||
>
|
||||
<div v-if="viewerStore.isDisconnected" class="space-y-4">
|
||||
<p class="text-sm font-medium text-muted-foreground">stream ended</p>
|
||||
<p class="text-sm font-medium text-muted-foreground">
|
||||
{{ $t("streamEnded") }}
|
||||
</p>
|
||||
<Button @click="handleReset" variant="outline">
|
||||
Enter another code
|
||||
{{ $t("enterAnotherCode") }}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
@@ -53,7 +55,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<p v-else class="text-muted-foreground/50 text-sm">
|
||||
enter code to join stream
|
||||
{{ $t("enterCodeToJoinStream") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +75,9 @@
|
||||
<div
|
||||
v-if="isConnected"
|
||||
class="absolute top-0 left-0 right-0 p-4 flex justify-between items-start transition-opacity bg-gradient-to-b from-black/50 to-transparent"
|
||||
:class="[controlsVisible ? 'opacity-100' : 'opacity-0 hover:opacity-100']"
|
||||
:class="[
|
||||
controlsVisible ? 'opacity-100' : 'opacity-0 hover:opacity-100',
|
||||
]"
|
||||
@click="resetControlsTimeout"
|
||||
@touchstart="showControls"
|
||||
>
|
||||
@@ -84,7 +88,7 @@
|
||||
@click="cleanupViewing"
|
||||
>
|
||||
<LogOut class="w-5 h-5" />
|
||||
Disconnect
|
||||
{{ $t("disconnect") }}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -94,7 +98,7 @@
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
<Maximize class="w-5 h-5" />
|
||||
Fullscreen
|
||||
{{ $t("fullscreen") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +323,7 @@ function resetControlsTimeout() {
|
||||
if (controlsHideTimeout) {
|
||||
clearTimeout(controlsHideTimeout);
|
||||
}
|
||||
|
||||
|
||||
controlsHideTimeout = setTimeout(() => {
|
||||
controlsVisible.value = false;
|
||||
}, 3000);
|
||||
@@ -344,4 +348,3 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-3xl font-bold">Presets</h1>
|
||||
<h1 class="text-3xl font-bold">{{ t('presets') }}</h1>
|
||||
<Button @click="navigateTo('/presets/new')">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Create New Preset
|
||||
{{ t('createNewPreset') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@@ -14,19 +14,16 @@
|
||||
<CardTitle class="flex items-center justify-between">
|
||||
<span>{{ presetUser.preset.name }}</span>
|
||||
<Badge v-if="presetUser.isDefault" variant="secondary"
|
||||
>Default</Badge
|
||||
>{{ t('default') }}</Badge
|
||||
>
|
||||
</CardTitle>
|
||||
<CardDescription
|
||||
>Created by {{ presetUser.preset.createdBy }}</CardDescription
|
||||
>{{ t('createdBy') }} {{ presetUser.preset.createdBy }}</CardDescription
|
||||
>
|
||||
</CardHeader>
|
||||
<CardContent class="grow">
|
||||
<div class="text-sm text-muted-foreground truncate">
|
||||
{{ presetUser.preset.iceServers.length }} ICE Server{{
|
||||
presetUser.preset.iceServers.length === 1 ? "" : "s"
|
||||
}}
|
||||
configured
|
||||
{{ presetUser.preset.iceServers.length }} {{ presetUser.preset.iceServers.length === 1 ? t('iceServerConfigured') : t('iceServersConfigured') }} {{ t('configured') }}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="flex justify-between gap-2 ml-auto">
|
||||
@@ -91,6 +88,7 @@ import EditPresetDialog from "~/components/app/EditPresetDialog.vue";
|
||||
import { Edit, Share2, Trash, Plus } from "lucide-vue-next";
|
||||
import type { ApiResponse } from "~/lib/types/PresetGetResponse";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { user } = useUser();
|
||||
const { data, refresh } = await useFetch<ApiResponse>("/api/presets", {
|
||||
cache: "no-cache",
|
||||
@@ -120,34 +118,34 @@ function editPreset(presetUser: any) {
|
||||
}
|
||||
|
||||
async function deletePreset(id: string) {
|
||||
if (!confirm("Are you sure you want to delete this preset?")) return;
|
||||
if (!confirm(t('deletePresetConfirm'))) return;
|
||||
|
||||
try {
|
||||
await $fetch(`/api/presets/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
toast.success("Preset deleted successfully");
|
||||
toast.success(t('presetDeletedSuccessfully'));
|
||||
refresh();
|
||||
} catch (error) {
|
||||
toast.error("Failed to delete preset");
|
||||
toast.error(t('failedToDeletePreset'));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleShare(preset: any) {
|
||||
if (!confirm("Do you want to share this preset?")) return;
|
||||
if (!confirm(t('sharePresetConfirm'))) return;
|
||||
try {
|
||||
const response = await $fetch(`/api/presets/${preset.id}/share`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.success) {
|
||||
toast.error("Failed to generate shareable link");
|
||||
toast.error(t('failedToGenerateShareableLink'));
|
||||
return;
|
||||
}
|
||||
const shareableLink = `${window.location.origin}/presets/shared/${preset.id}`;
|
||||
navigator.clipboard.writeText(shareableLink);
|
||||
toast.success("Link copied to clipboard");
|
||||
toast.success(t('linkCopiedToClipboard'));
|
||||
} catch (error) {
|
||||
toast.error("Failed to share preset");
|
||||
toast.error(t('failedToSharePreset'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
||||
<div class="flex space-x-4 items-center">
|
||||
<Button @click="startScreenShare"> screenshare </Button>
|
||||
<Button @click="startScreenShare"> {{ $t('screenshare') }} </Button>
|
||||
<PresetSelect />
|
||||
</div>
|
||||
<p v-if="streamerStore.code" class="font-mono">{{ streamerStore.code }}</p>
|
||||
|
||||
Reference in New Issue
Block a user