0
Fork 0
mirror of https://codeberg.org/SafeTwitch/safetwitch.git synced 2024-12-22 05:12:57 -05:00

Add translation support

This commit is contained in:
dragongoose 2023-06-13 12:08:43 -04:00
parent 14585e0144
commit fcac40082b
No known key found for this signature in database
GPG key ID: 50DB99B921579009
25 changed files with 2781 additions and 194 deletions

10
.env
View file

@ -1,3 +1,7 @@
VITE_BACKEND_DOMAIN=localhost:7000
VITE_INSTANCE_DOMAIN=localhost:5173
VITE_HTTPS=false
SAFETWITCH_BACKEND_DOMAIN=localhost:7000
SAFETWITCH_INSTANCE_DOMAIN=localhost:5173
SAFETWITCH_HTTPS=false
SAFETWITCH_DEFAULT_LOCALE=en
SAFETWITCH_FALLBACK_LOCALE=ja
VUE_APP_I18N_LOCALE=en
VUE_APP_I18N_FALLBACK_LOCALE=ja

6
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "safetwitch-translations"]
path = src/locales
url = https://codeberg.org/dragongoose/safetwitch-translations
[submodule "src/locales"]
path = src/locales
url = https://codeberg.org/dragongoose/safetwitch-translations

View file

@ -81,9 +81,9 @@ services:
ports:
- "8080:80"
environment:
- VITE_BACKEND_DOMAIN=localhost:7000
- VITE_INSTANCE_DOMAIN=localhost:80
- VITE_HTTPS=false
- SAFETWITCH_BACKEND_DOMAIN=localhost:7000
- SAFETWITCH_INSTANCE_DOMAIN=localhost:80
- SAFETWITCH_HTTPS=false
backend:
image: codeberg.org/dragongoose/safetwitch-backend
ports:

View file

@ -5,9 +5,11 @@ services:
ports:
- "8080:80"
environment:
- VITE_BACKEND_DOMAIN=localhost:7000
- VITE_INSTANCE_DOMAIN=localhost:80
- VITE_HTTPS=false
- SAFETWITCH_BACKEND_DOMAIN=localhost:7000
- SAFETWITCH_INSTANCE_DOMAIN=localhost:80
- SAFETWITCH_HTTPS=false
- SAFETWITCH_DEFAULT_LOCALE=en
- SAFETWITCH_FALLBACK_LOCALE=ja
backend:
image: codeberg.org/dragongoose/safetwitch-backend
ports:

View file

@ -7,7 +7,9 @@ services:
ports:
- "8080:80"
environment:
- VITE_BACKEND_DOMAIN=localhost:7000
- VITE_INSTANCE_DOMAIN=localhost:80
- VITE_HTTPS=false
- SAFETWITCH_BACKEND_DOMAIN=localhost:7000
- SAFETWITCH_INSTANCE_DOMAIN=localhost:80
- SAFETWITCH_HTTPS=false
- SAFETWITCH_DEFAULT_LOCALE=en
- SAFETWITCH_FALLBACK_LOCALE=ja

8
env.d.ts vendored
View file

@ -1,9 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_BACKEND_DOMAIN: string
readonly VITE_INSTANCE_DOMAIN: string
readonly VITE_HTTPS: string
readonly SAFETWITCH_BACKEND_DOMAIN: string
readonly SAFETWITCH_INSTANCE_DOMAIN: string
readonly SAFETWITCH_HTTPS: string
readonly SAFETWITCH_DEFAULT_LOCALE: string
readonly SAFETWITCH_FALLBACK_LOCALE: string
// more env variables...
}

2774
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,26 +3,31 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
"build-only": "vite build",
"dev": "vite",
"format": "prettier --write src/",
"i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.json\"",
"preview": "vite preview",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@dragongoose/streamlink": "^1.0.2",
"@intlify/unplugin-vue-i18n": "^0.11.0",
"@tailwindcss/forms": "^0.5.3",
"@vue/cli-shared-utils": "^5.0.8",
"oh-vue-icons": "^1.0.0-rc3",
"video.js": "^8.0.4",
"videojs-contrib-quality-levels": "^3.0.0",
"videojs-hls-quality-selector": "^1.1.4",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@catppuccin/tailwindcss": "^0.1.1",
"@intlify/vue-i18n-loader": "^3.0.0",
"@rushstack/eslint-patch": "^1.2.0",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "^18.14.2",
@ -42,6 +47,7 @@
"tailwindcss": "^3.2.7",
"typescript": "~4.8.4",
"vite": "^4.1.4",
"vue-cli-plugin-i18n": "~2.3.2",
"vue-tsc": "^1.2.0"
}
}

View file

@ -3,6 +3,7 @@
import videojs from 'video.js'
import 'videojs-contrib-quality-levels'
import type { QualityLevelList, QualityLevel } from 'videojs-contrib-quality-levels'
import { useI18n } from 'vue-i18n'
export const createQualitySelector = (player: any) => {
const qualityLevels: QualityLevelList = player.qualityLevels()
@ -10,6 +11,8 @@ export const createQualitySelector = (player: any) => {
const MenuItem = videojs.getComponent('MenuItem')
let formatedQualities: { name: string; index: number; id: string }[]
let t = useI18n()
const setQuality = (id: string) => {
const found = formatedQualities.find((i) => i.id === id)
for (const quality of qualityLevels.levels_) {
@ -56,7 +59,7 @@ export const createQualitySelector = (player: any) => {
const updateLevels = (items: { name: string; index: number; id: string; }[]) => {
player.controlBar.removeChild('CustomMenuButton')
player.controlBar.addChild('CustomMenuButton', {
title: 'Qualities',
title: t("player.quality"),
items: formatedQualities
})
}

View file

@ -7,12 +7,12 @@ export default {}
class="flex flex-col max-w-prose justify-center text-center mx-auto p-6 bg-ctp-crust rounded-lg text-white"
>
<div class="mb-6">
<h1 class="font-bold text-5xl">oops...</h1>
<p class="font-bold text-3xl">this wasn't supposed to happen</p>
<h1 class="font-bold text-5xl">{{ $t("error.oops") }}</h1>
<p class="font-bold text-3xl">{{ $t("error.notsupposedtohappen") }}</p>
</div>
<p class="text-xl">
the server was encountered an error while retriving the data, and now we're here :3
{{ $t("error.serverexplain") }}
</p>
</div>
</template>

View file

@ -56,7 +56,7 @@ export default {
class="text-white text-sm font-bold p-2 py-1 rounded-md bg-purple-600"
>
<v-icon name="bi-heart-fill" scale="0.85"></v-icon>
<span v-if="isFollowing"> Unfollow </span>
<span v-else> Follow </span>
<span v-if="isFollowing"> {{ $t("streamer.unfollow") }} </span>
<span v-else> {{ $t("streamer.follow") }} </span>
</button>
</template>

View file

@ -0,0 +1,20 @@
<template>
<div class="flex">
<select v-model="$i18n.locale" class="my-auto p-0 pr-9 bg-transparent border-0" :selected="$i18n.locale">
<option v-for="(lang, i) in langs" :key="`Lang${i}`" :value="lang">
{{ names[i] }}
</option>
</select>
</div>
</template>
<script lang="ts">
export default {
setup() {
return {
langs: ['en'],
names: ['English']
}
}
}
</script>

View file

@ -1,7 +1,7 @@
<template>
<div class="flex mx-auto justify-center bg-ctp-crust rounded-lg w-2/3 p-2 text-white">
<div class="flex space-x-3">
<h1 class="text-4xl font-bold">Searching...</h1>
<h1 class="text-4xl font-bold">{{ $t("main.searching") }}</h1>
<v-icon name="fa-circle-notch" class="animate-spin w-10 h-10"></v-icon>
</div>
</div>

View file

@ -1,8 +1,10 @@
<script lang="ts">
import SearchBar from './SearchBar.vue'
import LanguageSwitcher from './LanguageSwitcher.vue'
export default {
components: {
SearchBar
SearchBar,
LanguageSwitcher
}
}
</script>
@ -18,8 +20,9 @@ export default {
</div>
<ul class="inline-flex space-x-6 font-medium">
<a href="https://codeberg.org/dragongoose/safetwitch">Code</a>
<router-link to="/privacy">Privacy</router-link>
<a href="https://codeberg.org/dragongoose/safetwitch">{{ $t("nav.code") }}</a>
<router-link to="/privacy">{{ $t("nav.privacy") }}</router-link>
<language-switcher></language-switcher>
</ul>
</div>
</template>

View file

@ -16,9 +16,9 @@ export default {
<template>
<div class="relative hidden md:block">
<label for="searchBar" class="hidden">Search</label>
<label for="searchBar" class="hidden">{{ $t("main.search") }}</label>
<v-icon name="io-search-outline" class="text-black absolute my-auto inset-y-0 left-2"></v-icon>
<input type="text" placeholder="Search"
<input type="text" :placeholder="$t('main.search')"
@keyup.enter=redirectToSearch v-model="searchInput"
class="rounded-md p-1 pl-8 text-black" ref="searchInput" />
</div>

View file

@ -26,7 +26,7 @@ export default {
let streamData: Stream | null = null
if (!props.stream && props.name) {
const streamDataFetch = await fetch(
`${protocol}${import.meta.env.VITE_BACKEND_DOMAIN}/api/users/${props.name}`
`${protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/users/${props.name}`
)
const data = (await streamDataFetch.json()).data
@ -38,7 +38,7 @@ export default {
streamData = props.stream as Stream
}
const frontend_url = protocol + import.meta.env.VITE_INSTANCE_DOMAIN
const frontend_url = protocol + import.meta.env.SAFETWITCH_INSTANCE_DOMAIN
return {
frontend_url,

View file

@ -21,11 +21,11 @@ export default {
let messages: Ref<ParsedMessage[]> = ref([])
const protocol = inject('protocol')
const wsProtocol = protocol === 'https://' ? 'wss://' : 'ws://'
const badgesFetch = await fetch(`${protocol}${import.meta.env.VITE_BACKEND_DOMAIN}/api/badges?channelName=${props.channelName}`)
const badgesFetch = await fetch(`${protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/badges?channelName=${props.channelName}`)
let badges: Badge[] = (await badgesFetch.json()).data
return {
ws: new WebSocket(`${wsProtocol}${import.meta.env.VITE_BACKEND_DOMAIN}`),
ws: new WebSocket(`${wsProtocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}`),
messages,
badges,
props,
@ -37,7 +37,7 @@ export default {
this.ws.onmessage = (message) => {
if (message.data == 'OK') {
chatStatusMessage.textContent = `Connected to ${this.channelName}`
chatStatusMessage.textContent = this.$t("chat.connected", {username: this.channelName})
} else {
this.messages.push(parseMessage(message.data, this.badges))
this.clearMessages()
@ -92,7 +92,7 @@ export default {
>
<li>
<p ref="initConnectingStatus" class="text-gray-500 text-sm italic">
Connecting to {{ channelName }}.
{{ $t("chat.connecting", { username: channelName }) }}
</p>
</li>
<li v-for="message in getChat()" :key="messages.indexOf(message)">
@ -116,11 +116,11 @@ export default {
</div>
<div v-else-if="message.type === 'CLEARMSG'" class="text-white inline-flex">
<p class="text-sm text-gray-500 italic"> Message by {{ message.data.username }} removed </p>
<p class="text-sm text-gray-500 italic"> {{ $t("chat.removed", { username: message.data.username }) }} </p>
</div>
<div v-else-if="message.type === 'USERNOTICE'" class="text-white inline-flex bg-ctp-pink bg-opacity-50 p-1 rounded-md">
<p> <strong>{{ message.data.username }}</strong> has just resubbed for {{ message.data.months }} months!</p>
<p> {{ $t("chat.resub", { username: message.data.username, duration : message.data.months }) }} </p>
</div>
<div v-else class="text-white">

12
src/i18n.ts Normal file
View file

@ -0,0 +1,12 @@
import { createI18n } from 'vue-i18n'
import en from '@/locales/en.json'
export default createI18n({
legacy: false,
locale: import.meta.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: import.meta.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
globalInjection: true,
messages: {
'en': en,
}
})

View file

@ -2,14 +2,15 @@ import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/index.css'
import i18n from "./i18n"
const app = createApp(App)
const app = createApp(App).use(i18n)
// Add protocol variable
// For some reason, import.meta.env.VITE_HTTPS === "true"
// returns false, even if it is true.
// Making a copy of the variable seems to work
const https = (import.meta.env.VITE_HTTPS.slice() === "true")
const https = (import.meta.env.SAFETWITCH_HTTPS.slice() === "true")
app.provide('protocol', https ? 'https://' : 'http://')

View file

@ -20,7 +20,7 @@ export default {
async mounted() {
try {
const res = await fetch(
`${this.protocol}${import.meta.env.VITE_BACKEND_DOMAIN}/api/discover/${this.game}`
`${this.protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/discover/${this.game}`
)
const rawData = await res.json()
if (rawData.status === "ok") {
@ -46,7 +46,7 @@ export default {
const cursor = streams[streams.length - 1].cursor
if (!cursor) return
const res = await fetch(
`${this.protocol}${import.meta.env.VITE_BACKEND_DOMAIN}/api/discover/${this.game}/?cursor=${cursor}`
`${this.protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/discover/${this.game}/?cursor=${cursor}`
)
if (!res.ok) {
throw new Error('Failed to fetch data')
@ -89,8 +89,8 @@ export default {
<div class="hidden md:block">
<div>
<div class="inline-flex my-1 space-x-3">
<p class="font-bold text-white text-lg">Followers: {{ abbreviate(data.followers) }}</p>
<p class="font-bold text-white text-lg">Viewers: {{ abbreviate(data.viewers) }}</p>
<p class="font-bold text-white text-lg">{{ $t("main.followers") }}: {{ abbreviate(data.followers) }}</p>
<p class="font-bold text-white text-lg">{{ $t("main.viewers") }}: {{ abbreviate(data.viewers) }}</p>
</div>
<ul class="mb-5">
@ -109,8 +109,8 @@ export default {
<div class="md:hidden">
<div>
<div class="inline-flex my-1 space-x-3">
<p class="font-bold text-white text-lg">Followers: {{ abbreviate(data.followers) }}</p>
<p class="font-bold text-white text-lg">Viewers: {{ abbreviate(data.viewers) }}</p>
<p class="font-bold text-white text-lg">{{ $t("main.followers") }}: {{ abbreviate(data.followers) }}</p>
<p class="font-bold text-white text-lg">{{ $t("main.viewers") }}: {{ abbreviate(data.viewers) }}</p>
</div>
<ul class="mb-5">

View file

@ -51,7 +51,7 @@ export default {
const cursor = this.data[this.data.length - 1].cursor
if (!cursor) return
const res = await fetch(
`${this.protocol}${import.meta.env.VITE_BACKEND_DOMAIN}/api/discover/?cursor=${cursor}`
`${this.protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/discover/?cursor=${cursor}`
)
if (!res.ok) {
throw new Error('Failed to fetch data')
@ -76,7 +76,7 @@ export default {
}
try {
const res = await fetch(`${this.protocol}${import.meta.env.VITE_BACKEND_DOMAIN}/api/discover`)
const res = await fetch(`${this.protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/discover`)
const rawData = await res.json()
if (rawData.status === 'ok') {
@ -118,19 +118,19 @@ export default {
</div>
<div class="p-2">
<h1 class="font-bold text-5xl text-white">Discover</h1>
<p class="text-xl text-white">Sort through popular categories</p>
<h1 class="font-bold text-5xl text-white">{{ $t("home.discover") }}</h1>
<p class="text-xl text-white">{{ $t("home.discoverDescription") }}</p>
<div class="pt-5 inline-flex text-white">
<p class="mr-2 font-bold text-white">Filter by tag</p>
<p class="mr-2 font-bold text-white">{{ $t("home.tagDescription") }}</p>
<form class="relative">
<label for="searchBar" class="hidden">Search</label>
<label for="searchBar" class="hidden">{{ $t("main.search") }}</label>
<v-icon name="io-search-outline" class="absolute my-auto inset-y-0 left-2"></v-icon>
<input
type="text"
id="searchBar"
name="searchBar"
placeholder="Search"
:placeholder="$t('main.search')"
v-model="filterTags"
@keyup="filterSearches(filterTags)"
class="rounded-md p-1 pl-8 text-black bg-neutral-500 placeholder:text-white"

View file

@ -4,8 +4,8 @@ export default {}
<template>
<div class="flex flex-col items-center pt-10 font-bold text-5xl text-white">
<h1>oops....</h1>
<h1>this page wasn't found()</h1>
<h1>{{ $t("error.oops") }}</h1>
<h1>{{ $t("error.notfound") }}</h1>
<h2 class="text-4xl">maybe go <RouterLink to="/" class="text-gray-500">home</RouterLink>?</h2>
</div>
</template>

View file

@ -16,7 +16,7 @@ export default {
},
async mounted() {
try {
const res = await fetch(`${this.protocol}${import.meta.env.VITE_BACKEND_DOMAIN}/api/search/?query=${this.$route.query.query}`)
const res = await fetch(`${this.protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/search/?query=${this.$route.query.query}`)
const rawData = await res.json()
this.data = rawData.data

View file

@ -19,7 +19,7 @@ export default {
sources: [
{
src: `${protocol}${
import.meta.env.VITE_BACKEND_DOMAIN
import.meta.env.SAFETWITCH_BACKEND_DOMAIN
}/proxy/stream/${username}/hls.m3u8`,
type: 'application/vnd.apple.mpegurl'
}
@ -45,7 +45,7 @@ export default {
try {
const res = await fetch(
`${this.protocol}${import.meta.env.VITE_BACKEND_DOMAIN}/api/users/${username}`
`${this.protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/users/${username}`
)
const rawData = await res.json()
if (rawData.status === 'ok') {
@ -104,14 +104,14 @@ export default {
<span
v-if="data.isLive"
class="absolute top-16 right-[1.2rem] bg-ctp-red font-bold text-sm p-1.5 py-0.5 rounded-md"
>LIVE</span
>{{ $t("main.live") }}</span
>
</div>
<div class="ml-3 content-between">
<h1 class="text-2xl md:text-4xl font-bold">{{ data.username }}</h1>
<h1 v-if="!data.stream" class="font-bold text-md self-end">
{{ abbreviate(data.followers) }} Followers
{{ abbreviate(data.followers) }} {{ $t("main.live") }}
</h1>
<div v-else class="w-[14rem] md:w-[17rem]">
<p class="text-sm font-bold text-gray-200 self-end">
@ -144,13 +144,13 @@ export default {
<div class="pt-2 inline-flex">
<follow-button :username="data.username"></follow-button>
<p class="align-baseline font-bold ml-3">{{ abbreviate(data.followers) }} Followers</p>
<p class="align-baseline font-bold ml-3">{{ abbreviate(data.followers) }} {{ $t("main.followers") }}</p>
</div>
</div>
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
<div class="inline-flex w-full">
<span class="pr-3 font-bold text-3xl">About</span>
<span class="pr-3 font-bold text-3xl">{{ $t("streamer.about") }}</span>
</div>
<p class="mb-5">{{ data.about }}</p>

View file

@ -2,10 +2,17 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import { dirname, resolve } from 'node:path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
plugins: [
vue(),
VueI18nPlugin({
include: resolve(dirname(fileURLToPath(import.meta.url)), './src/locales/**'),
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
@ -21,5 +28,6 @@ export default defineConfig({
}
}
}
}
},
envPrefix: 'SAFETWITCH_',
})