mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
feat: ✨ Implement internationalization
This commit is contained in:
parent
02d9869737
commit
8c3ddc2a28
23 changed files with 399 additions and 123 deletions
|
|
@ -41,6 +41,7 @@ import { cn } from "@/lib/utils";
|
|||
import type { Attachment, Emoji, Status } from "@versia/client/types";
|
||||
import { TriangleAlert } from "lucide-vue-next";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { languageTag } from "~/paraglide/runtime";
|
||||
import { type BooleanSetting, SettingIds } from "~/settings";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
import Attachments from "./attachments.vue";
|
||||
|
|
@ -75,7 +76,7 @@ const isOverflowing = computed(() => {
|
|||
|
||||
const characterCount = plainContent?.length;
|
||||
const formattedCharacterCount = characterCount
|
||||
? new Intl.NumberFormat("en-us").format(characterCount)
|
||||
? new Intl.NumberFormat(languageTag()).format(characterCount)
|
||||
: undefined;
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import type {
|
|||
UseTimeAgoUnitNamesDefault,
|
||||
} from "@vueuse/core";
|
||||
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||
import { languageTag } from "~/paraglide/runtime";
|
||||
import { SettingIds } from "~/settings";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import SmallCard from "../profiles/small-card.vue";
|
||||
|
|
@ -89,7 +90,7 @@ const timeAgo = useTimeAgo(createdAt, {
|
|||
invalid: "",
|
||||
} as UseTimeAgoMessages<UseTimeAgoUnitNamesDefault>,
|
||||
});
|
||||
const fullTime = new Intl.DateTimeFormat("en-US", {
|
||||
const fullTime = new Intl.DateTimeFormat(languageTag(), {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(createdAt);
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ const _delete = async () => {
|
|||
<DropdownMenuTrigger as-child>
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-56">
|
||||
<DropdownMenuContent class="min-w-56">
|
||||
<DropdownMenuLabel>Note Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
<Repeat class="size-4 text-primary" />
|
||||
<Avatar class="size-6 border" :src="avatar" :name="displayName" />
|
||||
<span class="font-semibold" v-render-emojis="emojis">{{ displayName }}</span>
|
||||
reblogged
|
||||
{{ m.large_vivid_horse_catch() }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Emoji } from "@versia/client/types";
|
||||
import { Repeat } from "lucide-vue-next";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
|
||||
const { url } = defineProps<{
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@
|
|||
<div v-else class="grid grid-cols-2 p-2 gap-2">
|
||||
<Button variant="outline" size="sm" @click="accept">
|
||||
<Check />
|
||||
Accept
|
||||
{{ m.slow_these_kestrel_sail() }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" @click="reject">
|
||||
<X />
|
||||
Reject
|
||||
{{ m.weary_steep_yak_embrace() }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -42,6 +42,7 @@ import { Check, Loader, X } from "lucide-vue-next";
|
|||
import { toast } from "vue-sonner";
|
||||
import CopyableText from "~/components/notes/copyable-text.vue";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
|
||||
const { follower } = defineProps<{
|
||||
|
|
@ -61,25 +62,25 @@ watch(relationship, () => {
|
|||
});
|
||||
|
||||
const accept = async () => {
|
||||
const id = toast.loading("Accepting follow request...");
|
||||
const id = toast.loading(m.cool_slimy_coyote_affirm());
|
||||
loading.value = true;
|
||||
|
||||
const { data } = await client.value.acceptFollowRequest(follower.id);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success("Follow request accepted.");
|
||||
toast.success(m.busy_awful_mouse_jump());
|
||||
relationship.value = data;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const reject = async () => {
|
||||
const id = toast.loading("Rejecting follow request...");
|
||||
const id = toast.loading(m.front_sunny_penguin_flip());
|
||||
loading.value = true;
|
||||
|
||||
const { data } = await client.value.rejectFollowRequest(follower.id);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success("Follow request rejected.");
|
||||
toast.success(m.green_flat_mayfly_trust());
|
||||
relationship.value = data;
|
||||
loading.value = false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useForm } from "vee-validate";
|
|||
import * as z from "zod";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
|
||||
const { instance } = defineProps<{
|
||||
instance: Instance;
|
||||
|
|
@ -26,15 +27,15 @@ const formSchema = toTypedSchema(
|
|||
identifier: z
|
||||
.string()
|
||||
.min(3, {
|
||||
message: "Must be at least 3 characters long",
|
||||
message: m.aware_house_dolphin_win(),
|
||||
})
|
||||
.or(
|
||||
z.string().email({
|
||||
message: "Must be a valid email address",
|
||||
message: m.weary_fresh_dragonfly_bless(),
|
||||
}),
|
||||
),
|
||||
password: z.string().min(3, {
|
||||
message: "Must be at least 3 characters long",
|
||||
message: m.aware_house_dolphin_win(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
|
@ -86,7 +87,7 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
<FormField v-slot="{ componentField }" name="identifier">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Email (or username)
|
||||
{{ m.fluffy_soft_wolf_cook() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="petergriffin" type="text" auto-capitalize="none"
|
||||
|
|
@ -99,7 +100,7 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
<FormField v-slot="{ componentField }" name="password">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Password
|
||||
{{ m.livid_bright_wallaby_quiz() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="hunter2" type="password" auto-capitalize="none"
|
||||
|
|
@ -111,7 +112,7 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
</FormField>
|
||||
<Button :disabled="isLoading" type="submit">
|
||||
<Loader v-if="isLoading" class="mr-2 h-4 w-4 animate-spin" />
|
||||
Sign In
|
||||
{{ m.fuzzy_sea_moth_absorb() }}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -121,7 +122,7 @@ const issuerRedirectUrl = (issuerId: string) => {
|
|||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="bg-background px-2 text-muted-foreground">
|
||||
Or continue with
|
||||
{{ m.tidy_tidy_cow_cut() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
{{ emoji.shortcode }}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{{ emoji.global ? "Global" : "Uploaded by you" }}
|
||||
{{ emoji.global ? m.lime_day_squid_pout() : m.witty_heroic_trout_cry() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter class="p-0" v-if="canEdit">
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
<DropdownMenuItem @click="editName">
|
||||
<TextCursorInput class="mr-2 h-4 w-4" />
|
||||
<span>Rename</span>
|
||||
{{ m.cuddly_such_swallow_hush() }}
|
||||
</DropdownMenuItem>
|
||||
<!-- <DropdownMenuItem @click="editCaption">
|
||||
<Captions class="mr-2 h-4 w-4" />
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
<DropdownMenuSeparator /> -->
|
||||
<DropdownMenuItem @click="_delete">
|
||||
<Delete class="mr-2 h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
{{ m.tense_quick_cod_favor() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -64,6 +64,7 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
|
||||
const { emoji } = defineProps<{
|
||||
emoji: Emoji;
|
||||
|
|
@ -75,21 +76,21 @@ const canEdit =
|
|||
|
||||
const editName = async () => {
|
||||
const result = await confirmModalService.confirm({
|
||||
title: "Enter a new shortcode",
|
||||
title: m.slimy_awful_florian_sail(),
|
||||
defaultValue: emoji.shortcode,
|
||||
confirmText: "Edit",
|
||||
confirmText: m.teary_antsy_panda_aid(),
|
||||
inputType: "text",
|
||||
});
|
||||
|
||||
if (result.confirmed) {
|
||||
const id = toast.loading("Updating shortcode...");
|
||||
const id = toast.loading(m.teary_tame_gull_bless());
|
||||
try {
|
||||
await client.value.updateEmoji(emoji.id, {
|
||||
shortcode: result.value,
|
||||
});
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success("Shortcode updated.");
|
||||
toast.success(m.gaudy_lime_bison_adore());
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
|
|
@ -97,11 +98,11 @@ const editName = async () => {
|
|||
};
|
||||
|
||||
const _delete = async () => {
|
||||
const id = toast.loading("Deleting emoji...");
|
||||
const id = toast.loading(m.weary_away_liger_zip());
|
||||
try {
|
||||
await client.value.deleteEmoji(emoji.id);
|
||||
toast.dismiss(id);
|
||||
toast.success("Emoji deleted.");
|
||||
toast.success(m.crisp_whole_canary_tear());
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Banner
|
||||
{{ m.bright_late_osprey_renew() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="file" accept="image/*" @change="handleChange" @blur="handleBlur" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Recommended size: over 1500x500px
|
||||
{{ m.great_level_lamb_sway() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -19,13 +19,13 @@
|
|||
<FormField v-slot="{ handleChange, handleBlur }" name="avatar">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Avatar
|
||||
{{ m.safe_icy_bulldog_quell() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="file" accept="image/*" @change="handleChange" @blur="handleBlur" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Recommended size: 400x400px
|
||||
{{ m.aware_quiet_opossum_catch() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -34,13 +34,13 @@
|
|||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Display Name
|
||||
{{ m.mild_known_mallard_jolt() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Custom emojis can be used here.
|
||||
{{ m.lime_dry_skunk_loop() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -49,13 +49,13 @@
|
|||
<FormField v-slot="{ componentField }" name="username">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Username
|
||||
{{ m.neat_silly_dog_prosper() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Changing this will break all links to your profile.
|
||||
{{ m.petty_plane_tadpole_earn() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -64,13 +64,13 @@
|
|||
<FormField v-slot="{ componentField }" name="bio">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Bio
|
||||
{{ m.next_caring_ladybug_hack() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows="10" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Markdown and custom emojis are supported.
|
||||
{{ m.stale_just_anaconda_earn() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
|
@ -79,12 +79,14 @@
|
|||
<FormField v-slot="{ value, handleChange }" name="fields">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Custom Fields
|
||||
{{ m.aqua_mealy_toucan_pride() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div class="grid gap-4">
|
||||
<div v-for="(field, index) in value" :key="index" class="grid items-center grid-cols-[auto,repeat(3,minmax(0,1fr))] gap-2">
|
||||
<Button variant="destructive" size="icon" @click="handleChange([...value.slice(0, index), ...value.slice(index + 1)])">
|
||||
<div v-for="(field, index) in value" :key="index"
|
||||
class="grid items-center grid-cols-[auto,repeat(3,minmax(0,1fr))] gap-2">
|
||||
<Button variant="destructive" size="icon"
|
||||
@click="handleChange([...value.slice(0, index), ...value.slice(index + 1)])">
|
||||
<Trash />
|
||||
</Button>
|
||||
<Input v-model="field.name" placeholder="Name" @update:model-value="e => {
|
||||
|
|
@ -94,8 +96,9 @@
|
|||
handleChange([...value.slice(0, index), { name: field.name, value: e }, ...value.slice(index + 1)]);
|
||||
}" />
|
||||
</div>
|
||||
<Button type="button" variant="secondary" @click="handleChange([...value, { name: '', value: '' }])">
|
||||
Add Field
|
||||
<Button type="button" variant="secondary"
|
||||
@click="handleChange([...value, { name: '', value: '' }])">
|
||||
{{ m.front_north_eel_gulp() }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
|
@ -107,10 +110,10 @@
|
|||
<FormItem class="grid grid-cols-[1fr,auto] items-center p-6 gap-2">
|
||||
<CardHeader class="space-y-0.5 p-0">
|
||||
<FormLabel :as="CardTitle">
|
||||
Mark account as bot
|
||||
{{ m.gaudy_each_opossum_play() }}
|
||||
</FormLabel>
|
||||
<CardDescription>
|
||||
Is this account sending automated messages?
|
||||
{{ m.grassy_acidic_gadfly_cure() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<FormControl>
|
||||
|
|
@ -124,10 +127,10 @@
|
|||
<FormItem class="grid grid-cols-[1fr,auto] items-center p-6 gap-2">
|
||||
<CardHeader class="space-y-0.5 p-0">
|
||||
<FormLabel :as="CardTitle">
|
||||
Enable follow requests
|
||||
{{ m.dirty_moving_shark_emerge() }}
|
||||
</FormLabel>
|
||||
<CardDescription>
|
||||
Will require approval for new followers.
|
||||
{{ m.bright_fun_mouse_boil() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<FormControl>
|
||||
|
|
@ -141,10 +144,10 @@
|
|||
<FormItem class="grid grid-cols-[1fr,auto] items-center p-6 gap-2">
|
||||
<CardHeader class="space-y-0.5 p-0">
|
||||
<FormLabel :as="CardTitle">
|
||||
Allow account discovery
|
||||
{{ m.red_vivid_cuckoo_spark() }}
|
||||
</FormLabel>
|
||||
<CardDescription>
|
||||
Allow your account to be found in search results.
|
||||
{{ m.plain_zany_donkey_dart() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<FormControl>
|
||||
|
|
@ -179,6 +182,7 @@ import {
|
|||
import { Input } from "~/components/ui/input";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
|
||||
if (!identity.value) {
|
||||
throw new Error("Identity not found.");
|
||||
|
|
@ -195,7 +199,10 @@ const formSchema = toTypedSchema(
|
|||
v.size <=
|
||||
(identity.value?.instance.configuration.accounts
|
||||
.header_size_limit ?? 0),
|
||||
`Banner must be less than ${identity.value?.instance.configuration.accounts.header_size_limit} bytes`,
|
||||
m.civil_icy_ant_mend({
|
||||
size: identity.value?.instance.configuration.accounts
|
||||
.header_size_limit,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
avatar: z
|
||||
|
|
@ -205,7 +212,10 @@ const formSchema = toTypedSchema(
|
|||
v.size <=
|
||||
(identity.value?.instance.configuration.accounts
|
||||
.avatar_size_limit ?? 0),
|
||||
`Avatar must be less than ${identity.value?.instance.configuration.accounts.avatar_size_limit} bytes`,
|
||||
m.zippy_caring_raven_edit({
|
||||
size: identity.value?.instance.configuration.accounts
|
||||
.avatar_size_limit,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
name: z
|
||||
|
|
@ -216,10 +226,7 @@ const formSchema = toTypedSchema(
|
|||
),
|
||||
username: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-z0-9_-]+$/,
|
||||
"Username can only contain lowercase letters, numbers, underscores and hyphens",
|
||||
)
|
||||
.regex(/^[a-z0-9_-]+$/, m.still_upper_otter_dine())
|
||||
.max(
|
||||
identity.value.instance.configuration.accounts
|
||||
.max_username_characters,
|
||||
|
|
@ -251,7 +258,7 @@ const form = useForm({
|
|||
});
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
const id = toast.loading("Updating profile...");
|
||||
const id = toast.loading(m.jolly_noble_sloth_breathe());
|
||||
|
||||
const changedData = {
|
||||
display_name:
|
||||
|
|
@ -287,7 +294,7 @@ const handleSubmit = form.handleSubmit(async (values) => {
|
|||
Object.values(changedData).filter((v) => v !== undefined).length === 0
|
||||
) {
|
||||
toast.dismiss(id);
|
||||
toast.error("No changes");
|
||||
toast.error(m.tough_alive_niklas_promise());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +306,7 @@ const handleSubmit = form.handleSubmit(async (values) => {
|
|||
);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success("Profile updated");
|
||||
toast.success(m.spry_honest_kestrel_arrive());
|
||||
|
||||
if (identity.value) {
|
||||
identity.value.account = data;
|
||||
|
|
|
|||
|
|
@ -3,61 +3,61 @@
|
|||
<DropdownMenuTrigger as-child>
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-56">
|
||||
<DropdownMenuLabel>Profile Actions</DropdownMenuLabel>
|
||||
<DropdownMenuContent class="min-w-56">
|
||||
<DropdownMenuLabel>{{ m.spicy_loved_giraffe_empower() }}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem as="button" @click="copyText(account.username)">
|
||||
<AtSign class="mr-2 size-4" />
|
||||
<span>Copy username</span>
|
||||
{{ m.cool_dark_tapir_belong() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as="button" @click="copyText(JSON.stringify(account, null, 4))">
|
||||
<Code class="mr-2 size-4" />
|
||||
<span>Copy API data</span>
|
||||
{{ m.yummy_moving_scallop_sail() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as="button" @click="copyText(account.id)">
|
||||
<Hash class="mr-2 size-4" />
|
||||
<span>Copy ID</span>
|
||||
{{ m.sunny_zany_jellyfish_pop() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem as="button" @click="copyText(url)">
|
||||
<Link class="mr-2 size-4" />
|
||||
<span>Copy link</span>
|
||||
{{ m.ago_new_pelican_drip() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as="button" @click="copyText(account.url)">
|
||||
<Link class="mr-2 size-4" />
|
||||
<span>Copy link (origin)</span>
|
||||
{{ m.solid_witty_zebra_walk() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as="a" v-if="isRemote" target="_blank" rel="noopener noreferrer" :href="account.url">
|
||||
<ExternalLink class="mr-2 size-4" />
|
||||
<span>Open on remote</span>
|
||||
{{ m.active_trite_lark_inspire() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator v-if="isLoggedIn && !isMe" />
|
||||
<DropdownMenuGroup v-if="isLoggedIn && !isMe">
|
||||
<DropdownMenuItem as="button" @click="muteUser(account.id)">
|
||||
<VolumeX class="mr-2 size-4" />
|
||||
<span>Mute</span>
|
||||
{{ m.spare_wild_mole_intend() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as="button" @click="blockUser(account.id)">
|
||||
<Ban class="mr-2 size-4" />
|
||||
<span>Block</span>
|
||||
{{ m.misty_soft_sparrow_vent() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator v-if="isRemote" />
|
||||
<DropdownMenuGroup v-if="isRemote">
|
||||
<DropdownMenuItem as="button" @click="refresh">
|
||||
<RefreshCw class="mr-2 size-4" />
|
||||
<span>Refresh</span>
|
||||
{{ m.slow_chunky_chipmunk_hush() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator v-if="isLoggedIn && !isMe" />
|
||||
<DropdownMenuGroup v-if="isLoggedIn && !isMe">
|
||||
<DropdownMenuItem as="button" :disabled="true">
|
||||
<Flag class="mr-2 size-4" />
|
||||
<span>Report</span>
|
||||
{{ m.great_few_jaguar_rise() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
|
|
@ -87,6 +87,7 @@ import {
|
|||
VolumeX,
|
||||
} from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
|
||||
const { account } = defineProps<{
|
||||
account: Account;
|
||||
|
|
@ -98,14 +99,14 @@ const isLoggedIn = !!identity.value;
|
|||
const { copy } = useClipboard();
|
||||
const copyText = (text: string) => {
|
||||
copy(text);
|
||||
toast.success("Copied to clipboard");
|
||||
toast.success(m.flat_nice_worm_dream());
|
||||
};
|
||||
|
||||
const url = wrapUrl(`/@${account.acct}`);
|
||||
const isRemote = account.acct.includes("@");
|
||||
|
||||
const muteUser = async (userId: string) => {
|
||||
const id = toast.loading("Muting user...");
|
||||
const id = toast.loading(m.ornate_tidy_coyote_grow());
|
||||
await client.value.muteAccount(userId);
|
||||
toast.dismiss(id);
|
||||
|
||||
|
|
@ -113,7 +114,7 @@ const muteUser = async (userId: string) => {
|
|||
};
|
||||
|
||||
const blockUser = async (userId: string) => {
|
||||
const id = toast.loading("Blocking user...");
|
||||
const id = toast.loading(m.empty_smug_raven_bloom());
|
||||
await client.value.blockAccount(userId);
|
||||
toast.dismiss(id);
|
||||
|
||||
|
|
@ -121,10 +122,10 @@ const blockUser = async (userId: string) => {
|
|||
};
|
||||
|
||||
const refresh = async () => {
|
||||
const id = toast.loading("Requesting refresh...");
|
||||
const id = toast.loading(m.real_every_macaw_wish());
|
||||
await client.value.refetchAccount(account.id);
|
||||
toast.dismiss(id);
|
||||
|
||||
toast.success("Account refreshed");
|
||||
toast.success(m.many_cool_fox_love());
|
||||
};
|
||||
</script>
|
||||
|
|
@ -3,20 +3,20 @@
|
|||
<div class="flex flex-row flex-wrap gap-2 *:flex *:items-center *:gap-1 *:text-muted-foreground">
|
||||
<div>
|
||||
<CalendarDays class="size-4" />
|
||||
Joined <span class="text-primary font-semibold">{{ formattedCreationDate }}</span>
|
||||
{{ m.gross_fancy_platypus_seek() }} <span class="text-primary font-semibold">{{ formattedCreationDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-2 *:flex *:items-center *:gap-1 *:text-muted-foreground">
|
||||
<div>
|
||||
<span class="text-primary font-semibold">{{ noteCount }}</span> Notes
|
||||
<span class="text-primary font-semibold">{{ noteCount }}</span> {{ m.real_gray_stork_seek() }}
|
||||
</div>
|
||||
·
|
||||
<div>
|
||||
<span class="text-primary font-semibold">{{ followerCount }}</span> Followers
|
||||
<span class="text-primary font-semibold">{{ followerCount }}</span> {{ m.teal_helpful_parakeet_hike() }}
|
||||
</div>
|
||||
·
|
||||
<div>
|
||||
<span class="text-primary font-semibold">{{ followingCount }}</span> Following
|
||||
<span class="text-primary font-semibold">{{ followingCount }}</span> {{ m.aloof_royal_samuel_startle() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -24,6 +24,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { CalendarDays } from "lucide-vue-next";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { languageTag } from "~/paraglide/runtime";
|
||||
|
||||
const { creationDate } = defineProps<{
|
||||
creationDate: Date;
|
||||
|
|
@ -32,7 +34,7 @@ const { creationDate } = defineProps<{
|
|||
followingCount: number;
|
||||
}>();
|
||||
|
||||
const formattedCreationDate = new Intl.DateTimeFormat("en-US", {
|
||||
const formattedCreationDate = new Intl.DateTimeFormat(languageTag(), {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(creationDate);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
@click="relationship?.following ? unfollow() : follow()">
|
||||
<Loader v-if="isLoading" class="animate-spin" />
|
||||
<span v-else>
|
||||
{{ relationship?.following ? "Unfollow" : relationship?.requested ? "Requested" : "Follow" }}
|
||||
{{ relationship?.following ? m.brief_upper_otter_cuddle() : relationship?.requested ? m.weak_bright_larva_grasp() : m.lazy_major_loris_grasp() }}
|
||||
</span>
|
||||
</Button>
|
||||
<ProfileActions :account="account">
|
||||
|
|
@ -29,10 +29,10 @@
|
|||
</CopyableText>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-2 -mx-2" v-if="isDeveloper || account.bot || roles.length > 0">
|
||||
<ProfileBadge v-if="isDeveloper" name="Versia Developer" description="This user is a Versia developer."
|
||||
<ProfileBadge v-if="isDeveloper" :name="m.nice_bad_grizzly_coax()" :description="m.honest_jolly_shell_blend()"
|
||||
:verified="true" />
|
||||
<ProfileBadge v-if="account.bot" name="Automated"
|
||||
description="This account is not operated as living entity." />
|
||||
<ProfileBadge v-if="account.bot" :name="m.merry_red_shrimp_bump()"
|
||||
:description="m.sweet_mad_jannes_create()" />
|
||||
<ProfileBadge v-for="role in roles" :key="role.id" :name="role.name" :description="role.description"
|
||||
:icon="role.icon" />
|
||||
</div>
|
||||
|
|
@ -55,6 +55,7 @@ import CopyableText from "~/components/notes/copyable-text.vue";
|
|||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardFooter, CardTitle } from "~/components/ui/card";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { SettingIds } from "~/settings";
|
||||
import { confirmModalService } from "../modals/composable";
|
||||
import ProfileActions from "./profile-actions.vue";
|
||||
|
|
@ -84,44 +85,48 @@ const confirmFollows = useSetting(SettingIds.ConfirmFollow);
|
|||
const follow = async () => {
|
||||
if (confirmFollows.value.value) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: "Follow user",
|
||||
message: `Are you sure you want to follow @${account.acct}?`,
|
||||
confirmText: "Follow",
|
||||
cancelText: "Cancel",
|
||||
title: m.many_fair_capybara_imagine(),
|
||||
message: m.mellow_yummy_jannes_cuddle({
|
||||
acct: `@${account.acct}`,
|
||||
}),
|
||||
confirmText: m.cuddly_even_tern_loop(),
|
||||
cancelText: m.soft_bold_ant_attend(),
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
if (!confirmation.confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const id = toast.loading("Following user...");
|
||||
const id = toast.loading(m.quick_basic_peacock_bubble());
|
||||
const { data } = await client.value.followAccount(account.id);
|
||||
toast.dismiss(id);
|
||||
|
||||
relationship.value = data;
|
||||
toast.success("User followed");
|
||||
toast.success(m.awake_quick_cuckoo_smile());
|
||||
};
|
||||
|
||||
const unfollow = async () => {
|
||||
if (confirmFollows.value.value) {
|
||||
const confirmation = await confirmModalService.confirm({
|
||||
title: "Unfollow user",
|
||||
message: `Are you sure you want to unfollow @${account.acct}?`,
|
||||
confirmText: "Unfollow",
|
||||
cancelText: "Cancel",
|
||||
title: m.funny_aloof_swan_loop(),
|
||||
message: m.white_best_dolphin_catch({
|
||||
acct: `@${account.acct}`,
|
||||
}),
|
||||
confirmText: m.cute_polite_oryx_blend(),
|
||||
cancelText: m.soft_bold_ant_attend(),
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
if (!confirmation.confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const id = toast.loading("Unfollowing user...");
|
||||
const id = toast.loading(m.big_safe_guppy_mix());
|
||||
const { data } = await client.value.unfollowAccount(account.id);
|
||||
toast.dismiss(id);
|
||||
|
||||
relationship.value = data;
|
||||
toast.success("User unfollowed");
|
||||
toast.success(m.misty_level_stingray_expand());
|
||||
};
|
||||
</script>
|
||||
|
|
@ -30,24 +30,24 @@
|
|||
</Button>
|
||||
<DropdownMenuItem @click="signInAction">
|
||||
<UserPlus />
|
||||
Add account
|
||||
{{ m.sunny_pink_hyena_walk() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator v-if="identity" />
|
||||
<DropdownMenuGroup v-if="identity">
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem :as="NuxtLink" :href="`/@${identity.account.username}`">
|
||||
<BadgeCheck />
|
||||
Account
|
||||
{{ m.factual_awful_hare_drip() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem @click="signOut()" v-if="identity">
|
||||
<LogOut />
|
||||
Log out
|
||||
{{ m.sharp_big_mallard_reap() }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem :as="NuxtLink" href="/register" v-else>
|
||||
<LogIn />
|
||||
Register
|
||||
{{ m.honest_few_baboon_pop() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -62,6 +62,7 @@ import {
|
|||
UserPlus,
|
||||
} from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { NuxtLink } from "#components";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import { Button } from "../ui/button";
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
'https://cdn.versia.pub/branding/icon.svg'
|
||||
" :name="instance?.title" />
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{{ instance?.title ?? 'Versia Server' }}</span>
|
||||
<span class="truncate text-xs">{{ "A Versia Server instance" }}</span>
|
||||
<span class="truncate font-semibold">{{ instance?.title ?? m.short_zippy_felix_kick() }}</span>
|
||||
<span class="truncate text-xs">{{ m.top_active_ocelot_cure() }}</span>
|
||||
</div>
|
||||
<!-- <ChevronsUpDown class="ml-auto" /> -->
|
||||
</SidebarMenuButton>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||
<SidebarGroupLabel>{{ m.trite_real_sawfish_drum() }}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in data.other.filter(
|
||||
i => i.requiresLogin ? !!identity : true,
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup v-if="identity" class="mt-auto">
|
||||
<SidebarGroupLabel>More</SidebarGroupLabel>
|
||||
<SidebarGroupLabel>{{ m.close_short_kitten_coax() }}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<Collapsible v-for="item in data.navMain" :key="item.title" as-child class="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
|
|
@ -73,12 +73,12 @@
|
|||
<Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
|
||||
v-if="identity" @click="useEvent('composer:open')">
|
||||
<Pen />
|
||||
<span class="group-data-[collapsible=icon]:hidden">Compose</span>
|
||||
<span class="group-data-[collapsible=icon]:hidden">{{ m.salty_aloof_turkey_nudge() }}</span>
|
||||
</Button>
|
||||
<Button variant="destructive" size="lg" class="w-full group-data-[collapsible=icon]:px-4"
|
||||
v-if="$pwa?.needRefresh" @click="$pwa?.updateServiceWorker(true)">
|
||||
<DownloadCloud />
|
||||
<span class="group-data-[collapsible=icon]:hidden">Update</span>
|
||||
<span class="group-data-[collapsible=icon]:hidden">{{ m.quaint_low_felix_pave() }}</span>
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
|
@ -119,6 +119,8 @@ import {
|
|||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
} from "~/components/ui/sidebar";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import { setLanguageTag } from "~/paraglide/runtime";
|
||||
import { type EnumSetting, SettingIds } from "~/settings";
|
||||
import Avatar from "../profiles/avatar.vue";
|
||||
import { Button } from "../ui/button";
|
||||
|
|
@ -126,31 +128,33 @@ import AccountSwitcher from "./account-switcher.vue";
|
|||
|
||||
const sidebarStyle = useSetting(SettingIds.SidebarStyle) as Ref<EnumSetting>;
|
||||
|
||||
setLanguageTag("fr");
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
{
|
||||
title: "Preferences",
|
||||
title: m.patchy_seemly_hound_grace(),
|
||||
url: "/preferences",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: "Account",
|
||||
title: m.factual_arable_jurgen_endure(),
|
||||
url: "/preferences/account",
|
||||
},
|
||||
{
|
||||
title: "Appearance",
|
||||
title: m.tough_clean_wolf_gleam(),
|
||||
url: "/preferences/appearance",
|
||||
},
|
||||
{
|
||||
title: "Behaviour",
|
||||
title: m.legal_best_tadpole_rise(),
|
||||
url: "/preferences/behaviour",
|
||||
},
|
||||
{
|
||||
title: "Emojis",
|
||||
title: m.novel_trite_sloth_adapt(),
|
||||
url: "/preferences/emojis",
|
||||
},
|
||||
{
|
||||
title: "Roles",
|
||||
title: m.safe_green_mink_cook(),
|
||||
url: "/preferences/roles",
|
||||
},
|
||||
],
|
||||
|
|
@ -158,31 +162,31 @@ const data = {
|
|||
],
|
||||
other: [
|
||||
{
|
||||
name: "Home",
|
||||
name: m.bland_chunky_sparrow_propel(),
|
||||
url: "/home",
|
||||
icon: House,
|
||||
requiresLogin: true,
|
||||
},
|
||||
{
|
||||
name: "Public",
|
||||
name: m.lost_trick_dog_grace(),
|
||||
url: "/public",
|
||||
icon: MapIcon,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
name: "Local",
|
||||
name: m.crazy_game_parrot_pave(),
|
||||
url: "/local",
|
||||
icon: BedSingle,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
name: "Global",
|
||||
name: m.real_tame_moose_greet(),
|
||||
url: "/global",
|
||||
icon: Globe,
|
||||
requiresLogin: false,
|
||||
},
|
||||
{
|
||||
name: "Notifications",
|
||||
name: m.that_patchy_mare_snip(),
|
||||
url: "/notifications",
|
||||
icon: Bell,
|
||||
requiresLogin: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue