add initial support of i18n

pull/6/head
Namekuji 2022-12-16 21:30:46 -05:00
rodzic 4884afbbbb
commit abee1fa2e9
17 zmienionych plików z 1143 dodań i 150 usunięć

Wyświetl plik

@ -1,12 +1,30 @@
# Domain of your Audon server
LOCAL_DOMAIN=audon.example.com LOCAL_DOMAIN=audon.example.com
#### Database Settings ####
# Host of MongoDB, set as [host]:[port]
DB_HOST=db:27017 DB_HOST=db:27017
# Database name in MongoDB
DB_NAME=audon DB_NAME=audon
# Username to connect to MongoDB
DB_USER=mongo DB_USER=mongo
# Password to connect to MongoDB
DB_PASS=mongo DB_PASS=mongo
#### Redis Settings ####
# Host of Redis, set as [host]:[port]
REDIS_HOST=redis:6379 REDIS_HOST=redis:6379
# Username to connect to Redis (optional)
REDIS_USER= REDIS_USER=
# Password to connect to Redis (optional)
REDIS_PASS= REDIS_PASS=
### LiveKit Settings ###
# Same as the keys field in livekit.yaml
LIVEKIT_API_KEY=devkey LIVEKIT_API_KEY=devkey
# Same as the keys field in livekit.yaml
LIVEKIT_API_SECRET=secret LIVEKIT_API_SECRET=secret
# Host of LiveKit, should be a different domain from LOCAL_DOMAIN
LIVEKIT_HOST=livekit.example.com LIVEKIT_HOST=livekit.example.com
# This value will be returned by Audon backend to browsers. Set the same domain as LIVEKIT_HOST if you are not sure.
LIVEKIT_LOCAL_DOMAIN=livekit.example.com LIVEKIT_LOCAL_DOMAIN=livekit.example.com

846
audon-fe/package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -9,6 +9,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@vuelidate/core": "^2.0.0", "@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0", "@vuelidate/validators": "^2.0.0",
"@vueuse/core": "^9.6.0", "@vueuse/core": "^9.6.0",
@ -18,6 +19,7 @@
"masto": "^4.9.0", "masto": "^4.9.0",
"pinia": "^2.0.26", "pinia": "^2.0.26",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vuetify": "^3.0.3" "vuetify": "^3.0.3"
}, },

Wyświetl plik

@ -1,28 +1,64 @@
<script setup> <script>
import { RouterView, RouterLink } from 'vue-router' import { RouterView, RouterLink } from "vue-router";
import locales from "./locales"
export default {
setup() {
return {
locales
}
},
methods: {
onLocaleChange() {
localStorage.setItem("locale", this.$i18n.locale);
}
}
}
</script> </script>
<template> <template>
<v-app class="fill-height"> <v-app class="fill-height">
<v-system-bar window> <v-system-bar window>
<v-row> <h2 class="text-center w-100">
<v-col class="text-center"> <RouterLink
<h2><RouterLink :to="{name: 'home'}" style="text-decoration: inherit; color: inherit;">Audon</RouterLink></h2> :to="{ name: 'home' }"
</v-col> style="text-decoration: inherit; color: inherit;"
</v-row> >Audon</RouterLink
<div style="position:fixed">v0.1.0-dev3</div> >
</h2>
</v-system-bar> </v-system-bar>
<v-main> <v-main>
<v-container class="fill-height"> <v-container class="fill-height">
<v-row align="center" justify="center" class="fill-height" id="mainArea"> <v-row
align="center"
justify="center"
class="fill-height"
id="mainArea"
>
<v-col> <v-col>
<v-responsive class="mx-auto" max-width="600px"> <v-responsive class="mx-auto" max-width="600px">
<RouterView /> <RouterView />
</v-responsive> </v-responsive>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
</v-main> </v-main>
<v-bottom-navigation :height="30">
<div class="w-100 d-flex justify-space-between align-center px-3">
<div>v0.1.0-dev3</div>
<div>
<select v-model="$i18n.locale" id="localeSelector" @change="onLocaleChange">
<option
v-for="locale in $i18n.availableLocales"
:key="`locale-${locale}`"
:value="locale"
>
{{ locales[locale] }}
</option>
</select>
</div>
</div>
</v-bottom-navigation>
</v-app> </v-app>
</template> </template>
@ -30,4 +66,9 @@ import { RouterView, RouterLink } from 'vue-router'
#app .v-application__wrap { #app .v-application__wrap {
min-height: 100%; min-height: 100%;
} }
#localeSelector option {
background: black;
color: white;
}
</style> </style>

Wyświetl plik

@ -0,0 +1,69 @@
about: "What is Audon?"
back: "Back"
login: "Sign In"
logout: "Sign out"
logoutConfirm: "Are you sure you want to sign out from Audon?"
loginRequired: "You need to sign in to view this page."
create: "Create"
cancel: "Cancel"
edit: "Edit"
save: "Save"
share: "Share"
copy: "Copy"
copied: "Copied"
enterRoom: "Enter"
leaveRoom: "Leave"
closeRoom: "Close"
close: "Close"
server: "Your Mastodon server"
addressRequired: "Enter your server address"
invalidAddress: "Invalid address"
serverNotFound: "Server not found"
createNewRoom: "Create New Room"
editRoom: "Edit Room"
comingFuture: "Coming with future update!"
form:
title: "Title"
titleRequired: "Room title required"
description: "Description"
restriction: "Who can join"
cohosts: "Cohosts"
cohostCanAlwaysJoin: "Cohosts can join regardless of this setting."
schedule: "Schedule at"
relationships:
everyone: "Everyone"
following: "Your following accounts"
follower: "Your follower accounts"
knowing: "Your following or follower accounts"
mutual: "Your mutual-follow accounts"
private: "Cohosts only"
shareRoomMessage: "Join my Audon room!\n{link}\n\nTitle: {title}"
roomReady:
header: "Your room is ready!"
message: "Your room \"{title}\" is now ready. Share the following URL with other participants."
errors:
notFound: "{value} not found"
alreadyAdded: "Already added"
connectionFailed: "Failed to connect"
alreadyConnected: "You have already joined this room on another device. Please wait for a minute to recoonect."
alreadyClosed: "This room has already been closed."
restriction:
following: "Only host's following accounts can join."
follower: "Only host's follower accounts can join."
knowing: "Only host's following or follower accounts can join."
mutual: "Only hots's mutual-follow accounts can join."
private: "Only cohosts can join."
default: "You are not allowed to join."
startListening: "Start Listening"
browserMuted: "Your sound is muted by the browser. Press @:startListening to continue."
speakRequest:
label: "Speak Requests"
dialog: "Are you sure you want to send a request to be a speaker?"
norequest: "No request"
sent: "Request sent!"
receive: "New speak request received!"
microphoneBlocked: "Your browser blocked recording."
closeRoomConfirm: "Are you sure you want to close this room?"
roomEvent:
closedByHost: "Host has closed this room."
removed: "You have been requested to leave."

Wyświetl plik

@ -0,0 +1,7 @@
// This defines display names of locales for the language selector in App.vue.
// Keys must match the file names of *.yaml in src/locales.
export default {
en: "English",
ja: "日本語"
}

Wyświetl plik

@ -0,0 +1,68 @@
about: "Audonについて"
back: "戻る"
login: "ログイン"
logout: "ログアウト"
logoutConfirm: "Audon からログアウトしますか?"
loginRequired: "続行するにはログインする必要があります。"
create: "作成"
cancel: "キャンセル"
edit: "編集"
save: "保存"
copy: "コピー"
copied: "コピーしました"
enterRoom: "入室"
leaveRoom: "退室"
closeRoom: "閉室"
close: "閉じる"
server: "Mastodon サーバー"
addressRequired: "アドレスを入力してください"
invalidAddress: "アドレスが有効ではありません"
serverNotFound: "サーバーが見つかりません"
createNewRoom: "部屋を作成"
editRoom: "部屋の編集"
comingFuture: "今後のアップデートで追加予定"
form:
title: "タイトル"
titleRequired: "部屋の名前を入力してください"
description: "説明"
restriction: "入室制限"
cohosts: "共同ホスト"
cohostCanAlwaysJoin: "共同ホストは制限に関わらず入室できます。"
schedule: "開始予約"
relationships:
everyone: "制限なし"
following: "あなたのフォロー限定"
follower: "あなたのフォロワー限定"
knowing: "あなたのフォローまたはフォロワー限定"
mutual: "あなたの相互フォロー限定"
private: "共同ホスト限定"
shareRoomMessage: "Audon で部屋を作りました!\n参加用リンク {link}\nタイトル {title}"
roomReady:
header: "お部屋の用意ができました!"
message: "{title} を作りました。参加者に以下の URL を共有してください。"
errors:
notFound: "{value} が見つかりません"
alreadyAdded: "すでに追加済みです"
connectionFailed: "接続できませんでした。"
alreadyConnected: "他のデバイスで入室済みです。切断された場合はしばらく待ってからやり直してください。"
alreadyClosed: "この部屋はすでに閉じられています。"
restriction:
following: "この部屋はホストのフォロー限定です。"
follower: "この部屋はホストのフォロワー限定です。"
knowing: "この部屋はホストのフォローまたはフォロワー限定です。"
mutual: "この部屋はホストの相互フォロー限定です。"
private: "この部屋は共同ホスト限定です。"
default: "入室が許可されていません。"
startListening: "視聴を始める"
browserMuted: "ブラウザの設定により無音になっています。続行するには @:startListening ボタンを押してください。"
speakRequest:
label: "発言リクエスト"
dialog: "発言をリクエストしますか?"
norequest: "リクエストはありません"
sent: "発言リクエストを送信しました"
receive: "新しい発言リクエストがあります"
microphoneBlocked: "ブラウザが録音を許可していません。"
closeRoomConfirm: "この部屋を閉じますか?"
roomEvent:
closedByHost: "ホストにより部屋が閉じられました。"
removed: "部屋から退去しました。"

Wyświetl plik

@ -4,7 +4,11 @@ import { createPinia } from "pinia";
import { createVuetify } from "vuetify"; import { createVuetify } from "vuetify";
import { aliases, mdi } from "vuetify/iconsets/mdi-svg"; import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
import { createI18n } from "vue-i18n";
import messages from "@intlify/unplugin-vue-i18n/messages";
import axios from "axios"; import axios from "axios";
import { split } from "lodash-es";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
@ -27,6 +31,18 @@ const vuetify = createVuetify({
}, },
}); });
const userLocale =
navigator.languages && navigator.languages.length
? navigator.languages[0]
: navigator.language;
const prefLocale = localStorage.getItem("locale");
const i18n = createI18n({
locale: prefLocale ?? split(userLocale, "-", 1)[0],
fallbackLocale: "en",
messages,
});
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;
// if audon server returns 401, display the login form // if audon server returns 401, display the login form
@ -52,7 +68,7 @@ router.beforeEach(async (to) => {
router.afterEach((to, from) => { router.afterEach((to, from) => {
const donStore = useMastodonStore(); const donStore = useMastodonStore();
if (!to.meta.noauth && !donStore.authorized) { if (!to.meta.noauth && !donStore.authorized) {
const query = to.name !== "home" ? {l: to.path} : {}; const query = to.name !== "home" ? { l: to.path } : {};
router.push({ name: "login", query }); // need to push in afterEach to get nonempty lastPath in LoginView.vue router.push({ name: "login", query }); // need to push in afterEach to get nonempty lastPath in LoginView.vue
} else if (to.name === "login" && donStore.authorized) { } else if (to.name === "login" && donStore.authorized) {
router.replace({ name: "home" }); router.replace({ name: "home" });
@ -61,6 +77,7 @@ router.afterEach((to, from) => {
const app = createApp(App); const app = createApp(App);
app.use(i18n);
app.use(createPinia()); app.use(createPinia());
app.use(vuetify); app.use(vuetify);
app.use(router); app.use(router);

Wyświetl plik

@ -17,7 +17,7 @@ export default {
<template> <template>
<div class="about"> <div class="about">
準備中 Under construction
<div> <div>
<RouterLink :to="{ name: 'home' }">Home</RouterLink> <RouterLink :to="{ name: 'home' }">Home</RouterLink>
</div> </div>

Wyświetl plik

@ -45,12 +45,12 @@ export default {
cohosts: [], cohosts: [],
relationship: "everyone", relationship: "everyone",
relOptions: [ relOptions: [
{ title: "制限なし", value: "everyone" }, { title: this.$t("form.relationships.everyone"), value: "everyone" },
{ title: "あなたのフォロー限定", value: "following" }, { title: this.$t("form.relationships.following"), value: "following" },
{ title: "あなたのフォロワー限定", value: "follower" }, { title: this.$t("form.relationships.follower"), value: "follower" },
{ title: "あなたのフォローまたはフォロワー限定", value: "knowing" }, { title: this.$t("form.relationships.knowing"), value: "knowing" },
{ title: "あなたの相互フォロー限定", value: "mutual" }, { title: this.$t("form.relationships.mutual"), value: "mutual" },
{ title: "共同ホスト限定", value: "private" }, { title: this.$t("form.relationships.private"), value: "private" },
], ],
scheduledAt: null, scheduledAt: null,
searchResult: null, searchResult: null,
@ -69,12 +69,12 @@ export default {
validations() { validations() {
return { return {
title: { title: {
required: helpers.withMessage("部屋の名前を入力してください", required), required: helpers.withMessage(this.$t("form.titleRequired"), required),
maxLength: maxLength(100) maxLength: maxLength(100),
}, },
description: { description: {
maxLength: maxLength(500) maxLength: maxLength(500),
} },
}; };
}, },
computed: { computed: {
@ -95,9 +95,7 @@ export default {
if (!donURL) return ""; if (!donURL) return "";
const url = new URL(donURL); const url = new URL(donURL);
const texts = [ const texts = [
"Audon で部屋を作りました!", this.$t("shareRoomMessage", { link: this.roomURL, title: this.title }),
`参加用リンク: ${this.roomURL}`,
`タイトル: ${this.title}`,
]; ];
if (this.description) if (this.description)
texts.push(truncate("\n" + this.description, { length: 200 })); texts.push(truncate("\n" + this.description, { length: 200 }));
@ -110,7 +108,7 @@ export default {
this.cohostSearch.cancel(); this.cohostSearch.cancel();
if (!val) return; if (!val) return;
if (some(this.cohosts, { finger: val })) { if (some(this.cohosts, { finger: val })) {
this.searchError.message = "すでに追加済みです"; this.searchError.message = this.$t("errors.alreadyAdded");
this.searchError.colour = "warning"; this.searchError.colour = "warning";
this.searchError.enabled = true; this.searchError.enabled = true;
return; return;
@ -141,7 +139,7 @@ export default {
this.searchResult = user; this.searchResult = user;
} catch (error) { } catch (error) {
if (error.isMastoError && error.statusCode === 404) { if (error.isMastoError && error.statusCode === 404) {
this.searchError.message = `${val} が見つかりません`; this.searchError.message = this.$t("errors.notFound", { value: val });
this.searchError.colour = "error"; this.searchError.colour = "error";
this.searchError.enabled = true; this.searchError.enabled = true;
} }
@ -172,7 +170,7 @@ export default {
remote_id: u.id, remote_id: u.id,
remote_url: u.url, remote_url: u.url,
})), })),
restriction: this.relationship restriction: this.relationship,
}; };
this.isSubmissionLoading = false; this.isSubmissionLoading = false;
try { try {
@ -195,16 +193,12 @@ export default {
<template> <template>
<v-dialog v-model="isDialogActive" persistent max-width="700"> <v-dialog v-model="isDialogActive" persistent max-width="700">
<v-alert <v-alert type="success" color="blue-gray" :title="$t('roomReady.header')">
type="success"
color="blue-gray"
title="お部屋の用意ができました!"
>
<div> <div>
{{ title }} を作りました参加者に以下の URL を共有してください {{ $t("roomReady.message", { title }) }}
</div> </div>
<div class="my-3"> <div class="my-3">
<h3 style="word-break: break-all;">{{ roomURL }}</h3> <h3 style="word-break: break-all">{{ roomURL }}</h3>
</div> </div>
<div> <div>
<v-btn <v-btn
@ -213,7 +207,7 @@ export default {
@click="onShareClick" @click="onShareClick"
color="#563ACC" color="#563ACC"
size="small" size="small"
>シェア</v-btn >{{ $t("share") }}</v-btn
> >
<v-btn <v-btn
@click="clipboard.copy(roomURL)" @click="clipboard.copy(roomURL)"
@ -222,7 +216,7 @@ export default {
:prepend-icon=" :prepend-icon="
clipboard.copied.value ? mdiClipboardCheck : mdiClipboardEdit clipboard.copied.value ? mdiClipboardCheck : mdiClipboardEdit
" "
>{{ clipboard.copied.value ? "コピーしました" : "コピー" }}</v-btn >{{ clipboard.copied.value ? $t("copied") : $t("copy") }}</v-btn
> >
</div> </div>
<div class="text-center mt-10 mb-1"> <div class="text-center mt-10 mb-1">
@ -230,7 +224,7 @@ export default {
color="indigo" color="indigo"
:to="{ name: 'room', params: { id: createdRoomID } }" :to="{ name: 'room', params: { id: createdRoomID } }"
size="large" size="large"
>入室</v-btn >{{ $t("enterRoom") }}</v-btn
> >
</div> </div>
</v-alert> </v-alert>
@ -239,7 +233,7 @@ export default {
<div> <div>
<v-btn class="ma-2" variant="text" color="blue" :to="{ name: 'home' }"> <v-btn class="ma-2" variant="text" color="blue" :to="{ name: 'home' }">
<v-icon start :icon="mdiArrowLeft"></v-icon> <v-icon start :icon="mdiArrowLeft"></v-icon>
戻る {{ $t("back") }}
</v-btn> </v-btn>
<v-snackbar <v-snackbar
v-model="searchError.enabled" v-model="searchError.enabled"
@ -251,12 +245,14 @@ export default {
{{ searchError.message }} {{ searchError.message }}
</v-snackbar> </v-snackbar>
<v-card :loading="isSubmissionLoading"> <v-card :loading="isSubmissionLoading">
<v-card-title class="text-center">部屋を新規作成</v-card-title> <v-card-title class="text-center">{{
$t("createNewRoom")
}}</v-card-title>
<v-card-text> <v-card-text>
<v-form> <v-form>
<v-text-field <v-text-field
v-model="title" v-model="title"
label="タイトル" :label="$t('form.title')"
:counter="100" :counter="100"
:error-messages="titleErrors" :error-messages="titleErrors"
required required
@ -267,18 +263,23 @@ export default {
auto-grow auto-grow
v-model="description" v-model="description"
rows="2" rows="2"
label="説明" :label="$t('form.description')"
:counter="500" :counter="500"
></v-textarea> ></v-textarea>
<v-select <v-select
:items="relOptions" :items="relOptions"
label="入室制限" :label="$t('form.restriction')"
v-model="relationship" v-model="relationship"
:messages="['共同ホストは制限に関わらず入室できます']" :messages="[$t('form.cohostCanAlwaysJoin')]"
></v-select> ></v-select>
<v-card class="my-3" variant="outlined"> <v-card class="my-3" variant="outlined">
<v-card-title class="text-subtitle-1">共同ホスト</v-card-title> <v-card-title class="text-subtitle-1">{{
<v-card-text v-if="cohosts.length > 0 || searchResult" class="py-0"> $t("form.cohosts")
}}</v-card-title>
<v-card-text
v-if="cohosts.length > 0 || searchResult"
class="py-0"
>
<template v-if="cohosts.length > 0"> <template v-if="cohosts.length > 0">
<v-list lines="two" variant="tonal"> <v-list lines="two" variant="tonal">
<v-list-item <v-list-item
@ -358,15 +359,15 @@ export default {
<v-text-field <v-text-field
type="datetime-local" type="datetime-local"
v-model="scheduledAt" v-model="scheduledAt"
label="開始予約" :label="$t('form.schedule')"
disabled disabled
:messages="['今後のアップデートで追加予定']" :messages="[$t('comingFuture')]"
></v-text-field> ></v-text-field>
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-btn block color="indigo" @click="onSubmit" variant="flat"> <v-btn block color="indigo" @click="onSubmit" variant="flat">
作成 {{ $t("create") }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>

Wyświetl plik

@ -15,7 +15,7 @@ export default {
}, },
methods: { methods: {
async onLogout() { async onLogout() {
if (!confirm("Audon からログアウトしますか?")) return; // if (!confirm(this.$t("logoutConfirm"))) return;
try { try {
const resp = await axios.post("/app/logout"); const resp = await axios.post("/app/logout");
@ -42,7 +42,7 @@ export default {
color="red" color="red"
@click="onLogout" @click="onLogout"
> >
ログアウト {{ $t("logout") }}
</v-btn> </v-btn>
</div> </div>
<div class="text-center my-10"> <div class="text-center my-10">
@ -65,7 +65,7 @@ export default {
<v-text-field v-mode="query"></v-text-field> <v-text-field v-mode="query"></v-text-field>
</v-col> --> </v-col> -->
<v-col cols="12"> <v-col cols="12">
<v-btn block :to="{ name: 'create' }" color="indigo">部屋を作成</v-btn> <v-btn block :to="{ name: 'create' }" color="indigo">{{ $t("createNewRoom") }}</v-btn>
</v-col> </v-col>
</v-row> </v-row>
</main> </main>

Wyświetl plik

@ -21,9 +21,9 @@ export default {
validations() { validations() {
return { return {
server: { server: {
required: helpers.withMessage("アドレスを入力してください", required), required: helpers.withMessage(this.$t("addressRequired"), required),
hostname: helpers.withMessage( hostname: helpers.withMessage(
"有効なアドレスを入力してください", this.$t("invalidAddress"),
validators.fqdn validators.fqdn
), ),
}, },
@ -56,7 +56,7 @@ export default {
} }
} catch (error) { } catch (error) {
if (error.response?.status === 404) { if (error.response?.status === 404) {
this.serverErr = "サーバーが見つかりません"; this.serverErr = this.$t("serverNotFound");
} }
} }
}, },
@ -69,14 +69,14 @@ export default {
</script> </script>
<template> <template>
<v-alert v-if="$route.query.warn" type="warning" variant="text"> <v-alert v-if="$route.query.l" type="warning" variant="text">
<div>ログインが必要です</div> <div>{{ $t("loginRequired") }}</div>
</v-alert> </v-alert>
<v-form ref="form" @submit.prevent="onSubmit" class="my-3" lazy-validation> <v-form ref="form" @submit.prevent="onSubmit" class="my-3" lazy-validation>
<v-text-field <v-text-field
v-model="server" v-model="server"
name="server" name="server"
label="Mastodon サーバー" :label="$t('server')"
placeholder="mastodon.example" placeholder="mastodon.example"
class="mb-2" class="mb-2"
:error-messages="serverErrors" :error-messages="serverErrors"
@ -85,10 +85,10 @@ export default {
clearable clearable
/> />
<v-btn block @click="onSubmit" :disabled="!v$.$dirty || v$.$error" <v-btn block @click="onSubmit" :disabled="!v$.$dirty || v$.$error"
>ログイン</v-btn >{{ $t("login") }}</v-btn
> >
</v-form> </v-form>
<div class="w-100 text-right"> <div class="w-100 text-right">
<RouterLink to="/about">利用規約</RouterLink> <RouterLink to="/about">{{ $t("about") }}</RouterLink>
</div> </div>
</template> </template>

Wyświetl plik

@ -59,7 +59,7 @@ export default {
editingRoomInfo: { editingRoomInfo: {
title: { title: {
required: helpers.withMessage( required: helpers.withMessage(
"部屋の名前を入力してください", this.$t("form.titleRequired"),
required required
), ),
maxLength: maxLength(100), maxLength: maxLength(100),
@ -102,12 +102,12 @@ export default {
restriction: "", restriction: "",
}, },
relOptions: [ relOptions: [
{ title: "制限なし", value: "everyone" }, { title: this.$t("form.relationships.everyone"), value: "everyone" },
{ title: "あなたのフォロー限定", value: "following" }, { title: this.$t("form.relationships.following"), value: "following" },
{ title: "あなたのフォロワー限定", value: "follower" }, { title: this.$t("form.relationships.follower"), value: "follower" },
{ title: "あなたのフォローまたはフォロワー限定", value: "knowing" }, { title: this.$t("form.relationships.knowing"), value: "knowing" },
{ title: "あなたの相互フォロー限定", value: "mutual" }, { title: this.$t("form.relationships.mutual"), value: "mutual" },
{ title: "共同ホスト限定", value: "private" }, { title: this.$t("form.relationships.private"), value: "private" },
], ],
participants: {}, participants: {},
cachedMastoData: {}, cachedMastoData: {},
@ -245,10 +245,10 @@ export default {
let message = ""; let message = "";
switch (reason) { switch (reason) {
case DisconnectReason.ROOM_DELETED: case DisconnectReason.ROOM_DELETED:
message = "ホストにより部屋が閉じられました。"; message = self.$t("roomEvent.closedByHost");
break; break;
case DisconnectReason.PARTICIPANT_REMOVED: case DisconnectReason.PARTICIPANT_REMOVED:
message = "部屋から退去しました"; message = self.$t("roomEvent.removed");
break; break;
case DisconnectReason.CLIENT_INITIATED: case DisconnectReason.CLIENT_INITIATED:
break; break;
@ -330,7 +330,7 @@ export default {
publishOpts publishOpts
); );
} catch { } catch {
alert("ブラウザが録音を許可していません"); alert(this.$t("microphoneBlocked"));
} }
} }
} catch (error) { } catch (error) {
@ -339,23 +339,22 @@ export default {
let message = ""; let message = "";
switch (error.response?.data) { switch (error.response?.data) {
case "following": case "following":
message = "この部屋はホストのフォロー限定です。"; message = this.$t("errors.restriction.following");
break; break;
case "follower": case "follower":
message = "この部屋はホストのフォロワー限定です。"; message = this.$t("errors.restriction.follower");
break; break;
case "knowing": case "knowing":
message = message = this.$t("errors.restriction.knowing");
"この部屋はホストのフォローまたはフォロワー限定です。";
break; break;
case "mutual": case "mutual":
message = "この部屋はホストの相互フォロー限定です。"; message = this.$t("errors.restriction.mutual");
break; break;
case "private": case "private":
message = "この部屋は共同ホスト限定です。"; message = this.$t("errors.restriction.private");
break; break;
default: default:
message = "入室が許可されていません。"; message = this.$t("errors.restriction.default");
} }
alert(message); alert(message);
break; break;
@ -363,12 +362,10 @@ export default {
pushNotFound(this.$route); pushNotFound(this.$route);
break; break;
case 406: case 406:
alert( alert(this.$t("errors.alreadyConnected"));
"他のデバイスで入室済みです。切断された場合はしばらく待ってからやり直してください。"
);
break; break;
case 410: case 410:
alert("この部屋はすでに閉じられています。"); alert(this.$t("errors.alreadyClosed"));
break; break;
default: default:
alert(error); alert(error);
@ -381,7 +378,7 @@ export default {
onResize() { onResize() {
const mainArea = document.getElementById("mainArea"); const mainArea = document.getElementById("mainArea");
const height = mainArea.clientHeight; const height = mainArea.clientHeight;
this.mainHeight = height > 700 ? 700 : window.innerHeight - 70; this.mainHeight = height > 700 ? 700 : window.innerHeight - 95;
}, },
isHost(identity) { isHost(identity) {
return identity === this.roomInfo.host?.audon_id; return identity === this.roomInfo.host?.audon_id;
@ -427,7 +424,7 @@ export default {
await this.publishDataToHostAndCohosts(data); await this.publishDataToHostAndCohosts(data);
}, },
async requestSpeak() { async requestSpeak() {
if (confirm("発言をリクエストしますか?")) { if (confirm(this.$t("speakRequest.dialog"))) {
await this.publishDataToHostAndCohosts({ kind: "speak_request" }); await this.publishDataToHostAndCohosts({ kind: "speak_request" });
this.showRequestedNotification = true; this.showRequestedNotification = true;
} }
@ -499,16 +496,15 @@ export default {
); );
} }
} catch { } catch {
alert("ブラウザが録音を許可していません"); alert(this.$t("microphoneBlocked"));
} }
} else { } else {
// alert("");
this.requestSpeak(); this.requestSpeak();
} }
}, },
async onRoomClose() { async onRoomClose() {
// TODO: change this from confirm to a vuetify thing // TODO: change this from confirm to a vuetify thing
if (confirm("この部屋を閉じますか?")) { if (confirm(this.$t("closeRoomConfirm"))) {
try { try {
await axios.delete(`/api/room/${this.roomID}`); await axios.delete(`/api/room/${this.roomID}`);
} catch (error) { } catch (error) {
@ -521,7 +517,7 @@ export default {
await this.roomClient.startAudio(); await this.roomClient.startAudio();
this.autoplayDisabled = false; this.autoplayDisabled = false;
} catch { } catch {
alert("接続できませんでした。退室します。"); alert(this.$t("errors.connectionFailed"));
await this.roomClient.disconnect(); await this.roomClient.disconnect();
} }
}, },
@ -557,11 +553,11 @@ export default {
<template> <template>
<v-dialog v-model="showEditDialog" max-width="500" persistent> <v-dialog v-model="showEditDialog" max-width="500" persistent>
<v-card> <v-card>
<v-card-title>部屋の編集</v-card-title> <v-card-title>{{ $t("editRoom") }}</v-card-title>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="editingRoomInfo.title" v-model="editingRoomInfo.title"
label="タイトル" :label="$t('form.title')"
:error-messages="titleErrors" :error-messages="titleErrors"
:counter="100" :counter="100"
required required
@ -572,14 +568,14 @@ export default {
auto-grow auto-grow
v-model="editingRoomInfo.description" v-model="editingRoomInfo.description"
rows="2" rows="2"
label="説明" :label="$t('form.description')"
:counter="500" :counter="500"
></v-textarea> ></v-textarea>
<v-select <v-select
:items="relOptions" :items="relOptions"
label="入室制限" :label="$t('form.restriction')"
v-model="editingRoomInfo.restriction" v-model="editingRoomInfo.restriction"
:messages="['共同ホストは制限に関わらず入室できます']" :messages="[$t('form.cohostCanAlwaysJoin')]"
></v-select> ></v-select>
</v-card-text> </v-card-text>
<v-divider></v-divider> <v-divider></v-divider>
@ -589,28 +585,32 @@ export default {
showEditDialog = false; showEditDialog = false;
editingRoomInfo = clone(roomInfo); editingRoomInfo = clone(roomInfo);
" "
>キャンセル</v-btn >{{ $t("cancel") }}</v-btn
> >
<v-btn @click="onEditSubmit"></v-btn> <v-btn @click="onEditSubmit">{{ $t("save") }}</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-dialog v-model="autoplayDisabled" max-width="500" persistent> <v-dialog v-model="autoplayDisabled" max-width="500" persistent>
<v-alert color="indigo"> <v-alert color="indigo">
<div class="mb-5"> <div class="mb-5">
ブラウザの設定により無音になっています続行するには視聴を始めるボタンを押してください {{ $t("browserMuted") }}
</div> </div>
<div class="text-center mb-3"> <div class="text-center mb-3">
<v-btn color="gray" @click="onStartListening"></v-btn> <v-btn color="gray" @click="onStartListening">{{
$t("startListening")
}}</v-btn>
</div> </div>
<div class="text-center"> <div class="text-center">
<v-btn variant="text" @click="roomClient.disconnect()">退</v-btn> <v-btn variant="text" @click="roomClient.disconnect()">{{
$t("leaveRoom")
}}</v-btn>
</div> </div>
</v-alert> </v-alert>
</v-dialog> </v-dialog>
<v-dialog v-model="showRequestDialog" max-width="500"> <v-dialog v-model="showRequestDialog" max-width="500">
<v-card max-height="600" class="d-flex flex-column"> <v-card max-height="600" class="d-flex flex-column">
<v-card-title>発言リクエスト</v-card-title> <v-card-title>{{ $t("speakRequest.label") }}</v-card-title>
<v-card-text class="flex-grow-1 overflow-auto py-0"> <v-card-text class="flex-grow-1 overflow-auto py-0">
<v-list v-if="speakRequests.size > 0" lines="two" variant="tonal"> <v-list v-if="speakRequests.size > 0" lines="two" variant="tonal">
<v-list-item <v-list-item
@ -651,11 +651,13 @@ export default {
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
</v-list> </v-list>
<p class="text-center py-3" v-else></p> <p class="text-center py-3" v-else>
{{ $t("speakRequest.norequest") }}
</p>
</v-card-text> </v-card-text>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-actions class="justify-end"> <v-card-actions class="justify-end">
<v-btn @click="showRequestDialog = false">閉じる</v-btn> <v-btn @click="showRequestDialog = false">{{ $t("close") }}</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -665,7 +667,7 @@ export default {
v-model="showRequestedNotification" v-model="showRequestedNotification"
color="info" color="info"
> >
<strong>発言リクエストを送信しました</strong> <strong>{{ $t("speakRequest.sent") }}</strong>
<template v-slot:actions> <template v-slot:actions>
<v-btn <v-btn
variant="text" variant="text"
@ -688,7 +690,7 @@ export default {
showRequestNotification = false; showRequestNotification = false;
" "
> >
<strong>新しい発言リクエストがあります</strong> <strong>{{ $t("speakRequest.receive") }}</strong>
</div> </div>
<template v-slot:actions> <template v-slot:actions>
<v-btn <v-btn
@ -717,12 +719,12 @@ export default {
</template> </template>
<v-list> <v-list>
<v-list-item <v-list-item
title="編集" :title="$t('edit')"
:prepend-icon="mdiPencil" :prepend-icon="mdiPencil"
@click="showEditDialog = true" @click="showEditDialog = true"
></v-list-item> ></v-list-item>
<v-list-item <v-list-item
title="閉室" :title="$t('closeRoom')"
:prepend-icon="mdiDoorClosed" :prepend-icon="mdiDoorClosed"
@click="onRoomClose" @click="onRoomClose"
></v-list-item> ></v-list-item>

Wyświetl plik

@ -4,9 +4,17 @@ import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import vuetify from "vite-plugin-vuetify"; import vuetify from "vite-plugin-vuetify";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue(), vuetify({ autoImport: true })], plugins: [
vue(),
vuetify({ autoImport: true }),
VueI18nPlugin({
include: "./src/locales/*.yaml",
}),
],
resolve: { resolve: {
alias: { alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)), "@": fileURLToPath(new URL("./src", import.meta.url)),

Wyświetl plik

@ -44,7 +44,7 @@ services:
audon: audon:
build: . build: .
image: namekuji/audon image: nmkj/audon
env_file: env_file:
- .env.production - .env.production
restart: unless-stopped restart: unless-stopped

Wyświetl plik

@ -194,7 +194,7 @@ func joinRoomHandler(c echo.Context) (err error) {
// check room restriction // check room restriction
if room.IsPrivate() && !canTalk { if room.IsPrivate() && !canTalk {
return ErrOperationNotPermitted return c.String(http.StatusForbidden, string(room.Restriction))
} }
if !canTalk && (room.IsFollowingOnly() || room.IsFollowerOnly() || room.IsFollowingOrFollowerOnly() || room.IsMutualOnly()) { if !canTalk && (room.IsFollowingOnly() || room.IsFollowerOnly() || room.IsFollowingOrFollowerOnly() || room.IsMutualOnly()) {
mastoClient, _ := getMastodonClient(c) mastoClient, _ := getMastodonClient(c)