feat: Implement internationalization

This commit is contained in:
Jesse Wierzbinski 2024-12-07 20:24:09 +01:00
parent 02d9869737
commit 8c3ddc2a28
No known key found for this signature in database
23 changed files with 399 additions and 123 deletions

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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<{

View file

@ -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;
};

View file

@ -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>

View file

@ -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);
}

View file

@ -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;

View file

@ -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>

View file

@ -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>
&middot;
<div>
<span class="text-primary font-semibold">{{ followerCount }}</span> Followers
<span class="text-primary font-semibold">{{ followerCount }}</span> {{ m.teal_helpful_parakeet_hike() }}
</div>
&middot;
<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);

View file

@ -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>

View file

@ -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";

View file

@ -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,