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

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["inlang.vs-code-extension"]
}

View file

@ -75,6 +75,12 @@
"indentWidth": 4
},
"files": {
"ignore": ["node_modules/**/*", "dist/**/*", ".output", ".nuxt"]
"ignore": [
"node_modules/**/*",
"dist/**/*",
".output",
".nuxt",
"paraglide"
]
}
}

BIN
bun.lockb

Binary file not shown.

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,

112
messages/en.json Normal file
View file

@ -0,0 +1,112 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"bland_chunky_sparrow_propel": "Home",
"lost_trick_dog_grace": "Public",
"crazy_game_parrot_pave": "Local",
"real_tame_moose_greet": "Global",
"that_patchy_mare_snip": "Notifications",
"patchy_seemly_hound_grace": "Preferences",
"factual_arable_jurgen_endure": "Account",
"tough_clean_wolf_gleam": "Appearance",
"legal_best_tadpole_rise": "Behaviour",
"novel_trite_sloth_adapt": "Emojis",
"safe_green_mink_cook": "Roles",
"short_zippy_felix_kick": "Versia Server",
"top_active_ocelot_cure": "A Versia Server instance",
"trite_real_sawfish_drum": "Navigation",
"close_short_kitten_coax": "More",
"salty_aloof_turkey_nudge": "Compose",
"quaint_low_felix_pave": "Update",
"sunny_pink_hyena_walk": "Add account",
"factual_awful_hare_drip": "Account",
"sharp_big_mallard_reap": "Log out",
"honest_few_baboon_pop": "Register",
"spicy_loved_giraffe_empower": "Profile Actions",
"cool_dark_tapir_belong": "Copy username",
"yummy_moving_scallop_sail": "Copy API data",
"sunny_zany_jellyfish_pop": "Copy ID",
"ago_new_pelican_drip": "Copy link",
"solid_witty_zebra_walk": "Copy link (origin)",
"active_trite_lark_inspire": "Open on remote",
"spare_wild_mole_intend": "Mute",
"misty_soft_sparrow_vent": "Block",
"slow_chunky_chipmunk_hush": "Refresh",
"great_few_jaguar_rise": "Report",
"flat_nice_worm_dream": "Copied to clipboard",
"ornate_tidy_coyote_grow": "Muting user...",
"empty_smug_raven_bloom": "Blocking user...",
"real_every_macaw_wish": "Requesting refresh...",
"many_cool_fox_love": "Account refreshed",
"gross_fancy_platypus_seek": "Joined",
"real_gray_stork_seek": "Notes",
"teal_helpful_parakeet_hike": "Followers",
"aloof_royal_samuel_startle": "Following",
"brief_upper_otter_cuddle": "Unfollow",
"weak_bright_larva_grasp": "Requested",
"lazy_major_loris_grasp": "Follow",
"honest_jolly_shell_blend": "This user is a Versia developer.",
"nice_bad_grizzly_coax": "Versia Developer",
"merry_red_shrimp_bump": "Automated",
"sweet_mad_jannes_create": "This account is not operated as living entity.",
"many_fair_capybara_imagine": "Follow user",
"vivid_each_warthog_edit": "Are you sure you want to follow @${account.acct}?",
"cuddly_even_tern_loop": "Follow",
"soft_bold_ant_attend": "Cancel",
"quick_basic_peacock_bubble": "Following user...",
"awake_quick_cuckoo_smile": "User followed",
"funny_aloof_swan_loop": "Unfollow user",
"cute_polite_oryx_blend": "Unfollow",
"dirty_inclusive_meerkat_nudge": "Cancel",
"big_safe_guppy_mix": "Unfollowing user...",
"misty_level_stingray_expand": "User unfollowed",
"lime_day_squid_pout": "Global",
"witty_heroic_trout_cry": "Uploaded by you",
"cuddly_such_swallow_hush": "Rename",
"tense_quick_cod_favor": "Delete",
"slimy_awful_florian_sail": "Enter a new shortcode",
"teary_antsy_panda_aid": "Edit",
"teary_tame_gull_bless": "Updating shortcode...",
"gaudy_lime_bison_adore": "Shortcode updated.",
"weary_away_liger_zip": "Deleting emoji...",
"crisp_whole_canary_tear": "Emoji deleted.",
"mellow_yummy_jannes_cuddle": "Are you sure you want to follow {acct}?",
"white_best_dolphin_catch": "Are you sure you want to unfollow {acct}?",
"bright_late_osprey_renew": "Banner",
"great_level_lamb_sway": "Recommended size: over 1500x500px",
"safe_icy_bulldog_quell": "Avatar",
"aware_quiet_opossum_catch": "Recommended size: 400x400px",
"mild_known_mallard_jolt": "Display Name",
"lime_dry_skunk_loop": "Custom emojis can be used here.",
"neat_silly_dog_prosper": "Username",
"petty_plane_tadpole_earn": "Changing this will break all links to your profile.",
"next_caring_ladybug_hack": "Bio",
"stale_just_anaconda_earn": "Markdown and custom emojis are supported.",
"aqua_mealy_toucan_pride": "Custom Fields",
"front_north_eel_gulp": "Add field",
"gaudy_each_opossum_play": "Mark account as bot",
"grassy_acidic_gadfly_cure": "Is this account sending automated messages?",
"dirty_moving_shark_emerge": "Enable follow requests",
"bright_fun_mouse_boil": "Will require approval for new followers.",
"red_vivid_cuckoo_spark": "Allow account discovery",
"plain_zany_donkey_dart": "Allow your account to be found in search results.",
"jolly_noble_sloth_breathe": "Updating profile...",
"tough_alive_niklas_promise": "No changes",
"spry_honest_kestrel_arrive": "Profile updated",
"civil_icy_ant_mend": "Banner must be less than {size} bytes",
"zippy_caring_raven_edit": "Avatar must be less than {size} bytes",
"still_upper_otter_dine": "Username can only contain lowercase letters, numbers, underscores and hyphens",
"aware_house_dolphin_win": "Must be at least 3 characters long",
"weary_fresh_dragonfly_bless": "Must be a valid email address",
"sunny_novel_otter_glow": "Must be at least 3 characters long",
"fluffy_soft_wolf_cook": "Email (or username)",
"livid_bright_wallaby_quiz": "Password",
"fuzzy_sea_moth_absorb": "Sign In",
"tidy_tidy_cow_cut": "Or continue with",
"slow_these_kestrel_sail": "Accept",
"weary_steep_yak_embrace": "Reject",
"cool_slimy_coyote_affirm": "Accepting follow request...",
"busy_awful_mouse_jump": "Follow request accepted.",
"front_sunny_penguin_flip": "Rejecting follow request...",
"green_flat_mayfly_trust": "Follow request rejected.",
"large_vivid_horse_catch": "reblogged"
}

109
messages/fr.json Normal file
View file

@ -0,0 +1,109 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"bland_chunky_sparrow_propel": "Acceuil",
"lost_trick_dog_grace": "Public",
"crazy_game_parrot_pave": "Local",
"real_tame_moose_greet": "Global",
"that_patchy_mare_snip": "Notifications",
"patchy_seemly_hound_grace": "Préférences",
"factual_arable_jurgen_endure": "Compte",
"tough_clean_wolf_gleam": "Apparence",
"legal_best_tadpole_rise": "Comportement",
"novel_trite_sloth_adapt": "Émojis",
"safe_green_mink_cook": "Rôles",
"short_zippy_felix_kick": "Versia Server",
"top_active_ocelot_cure": "Une instance de Versia Server",
"trite_real_sawfish_drum": "Navigation",
"close_short_kitten_coax": "Plus",
"salty_aloof_turkey_nudge": "Composer",
"quaint_low_felix_pave": "Mettre à jour",
"sunny_pink_hyena_walk": "Ajouter un compte",
"factual_awful_hare_drip": "Compte",
"sharp_big_mallard_reap": "Se déconnecter",
"honest_few_baboon_pop": "Créer un compte",
"spicy_loved_giraffe_empower": "Actions du profil",
"cool_dark_tapir_belong": "Copier le nom d'utilisateur",
"yummy_moving_scallop_sail": "Copier les données de l'API",
"sunny_zany_jellyfish_pop": "Copier l'ID",
"ago_new_pelican_drip": "Copier le lien",
"solid_witty_zebra_walk": "Copier le lien (origine)",
"active_trite_lark_inspire": "Ouvrir l'origine",
"spare_wild_mole_intend": "Muter",
"misty_soft_sparrow_vent": "Bloquer",
"slow_chunky_chipmunk_hush": "Rafraîchir",
"great_few_jaguar_rise": "Signaler",
"flat_nice_worm_dream": "Copié dans le presse-papiers",
"ornate_tidy_coyote_grow": "Mutage de l'utilisateur...",
"empty_smug_raven_bloom": "Blocage de l'utilisateur...",
"real_every_macaw_wish": "Demande d'actualisation...",
"many_cool_fox_love": "Compte actualisé",
"gross_fancy_platypus_seek": "Inscrit en",
"real_gray_stork_seek": "Notes",
"teal_helpful_parakeet_hike": "Abonné•e•s",
"aloof_royal_samuel_startle": "Abonnements",
"brief_upper_otter_cuddle": "Se désabonner",
"weak_bright_larva_grasp": "Demandé",
"lazy_major_loris_grasp": "Suivre",
"honest_jolly_shell_blend": "Cet utilisateur est un développeur Versia.",
"nice_bad_grizzly_coax": "Développeur Versia",
"merry_red_shrimp_bump": "Automatisé",
"sweet_mad_jannes_create": "Ce compte n'est pas utilisé par une entité vivante.",
"many_fair_capybara_imagine": "Suivre",
"cuddly_even_tern_loop": "Suivre",
"soft_bold_ant_attend": "Annuler",
"quick_basic_peacock_bubble": "Abonnement en cours...",
"awake_quick_cuckoo_smile": "Utilisateur suivi",
"funny_aloof_swan_loop": "Se désabonner",
"cute_polite_oryx_blend": "Se désabonner",
"dirty_inclusive_meerkat_nudge": "Annuler",
"big_safe_guppy_mix": "Désabonnement...",
"misty_level_stingray_expand": "Utilisateur désabonné",
"lime_day_squid_pout": "Global",
"witty_heroic_trout_cry": "Ajouté par vous",
"cuddly_such_swallow_hush": "Renommer",
"tense_quick_cod_favor": "Supprimer",
"slimy_awful_florian_sail": "Entrez un nouveau nom",
"teary_antsy_panda_aid": "Modifier",
"teary_tame_gull_bless": "Mise à jour du nom...",
"gaudy_lime_bison_adore": "Nom mis à jour.",
"weary_away_liger_zip": "Suppression de l'emoji...",
"crisp_whole_canary_tear": "Emoji supprimé.",
"mellow_yummy_jannes_cuddle": "Êtes-vous sûr de vouloir suivre {acct} ?",
"white_best_dolphin_catch": "Etes-vous sûr de vouloir vous désabonner de {acct} ?",
"bright_late_osprey_renew": "Bannière",
"great_level_lamb_sway": "Taille recommandée : plus de 1500x500px",
"safe_icy_bulldog_quell": "Avatar",
"aware_quiet_opossum_catch": "Taille recommandée : 400x400px",
"mild_known_mallard_jolt": "Nom d'affichage",
"lime_dry_skunk_loop": "Des émojis personnalisés peuvent être utilisés ici.",
"neat_silly_dog_prosper": "Nom d'utilisateur",
"petty_plane_tadpole_earn": "Changer ce nom brisera tous les liens vers votre profil.",
"next_caring_ladybug_hack": "Bio",
"stale_just_anaconda_earn": "Le Markdown et les émojis personnalisés sont utilisables.",
"aqua_mealy_toucan_pride": "Champs personnalisés",
"front_north_eel_gulp": "Ajouter un champ",
"dirty_moving_shark_emerge": "Activer les demandes de suivi",
"bright_fun_mouse_boil": "Une approbation sera nécessaire pour les nouveaux abonnés.",
"red_vivid_cuckoo_spark": "Autoriser la découverte de compte",
"plain_zany_donkey_dart": "Permettez à votre compte d'être trouvé dans les résultats de recherche.",
"jolly_noble_sloth_breathe": "Mise à jour du profil...",
"tough_alive_niklas_promise": "Aucun changement",
"spry_honest_kestrel_arrive": "Profil mis à jour",
"civil_icy_ant_mend": "La bannière doit être inférieure à {size} octets",
"zippy_caring_raven_edit": "L'avatar doit être inférieur à {size} octets",
"still_upper_otter_dine": "Le nom d'utilisateur ne peut contenir que des lettres minuscules, des chiffres, des traits de soulignement et des tirets",
"aware_house_dolphin_win": "Doit comporter au moins 3 caractères",
"weary_fresh_dragonfly_bless": "Doit être une adresse e-mail valide",
"sunny_novel_otter_glow": "Doit comporter au moins 3 caractères",
"fluffy_soft_wolf_cook": "Email (ou nom d'utilisateur)",
"livid_bright_wallaby_quiz": "Mot de passe",
"fuzzy_sea_moth_absorb": "Se connecter",
"tidy_tidy_cow_cut": "Ou continuer avec",
"slow_these_kestrel_sail": "Accepter",
"weary_steep_yak_embrace": "Rejeter",
"cool_slimy_coyote_affirm": "Acceptation de la demande de suivi...",
"busy_awful_mouse_jump": "Demande de suivi acceptée.",
"front_sunny_penguin_flip": "Rejet de la demande de suivi...",
"green_flat_mayfly_trust": "Demande de suivi rejetée.",
"large_vivid_horse_catch": "a reblogué•e"
}

View file

@ -20,11 +20,12 @@
"url": "git+https://github.com/versia-pub/frontend.git"
},
"scripts": {
"build": "nuxt build",
"build": "paraglide-js compile --project ./project.inlang --outdir ./paraglide && nuxt build",
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bun --bun nuxt dev --https --https.cert config/versia-fe.localhost.pem --https.key config/versia-fe.localhost-key.pem --host versia-fe.localhost",
"generate": "nuxt generate",
"emojis:generate": "bun run utils/emojis.ts",
"postinstall": "nuxt prepare",
"postinstall": "paraglide-js compile --project ./project.inlang --outdir ./paraglide && nuxt prepare",
"rebuild-i18n": "paraglide-js compile --project ./project.inlang --outdir ./paraglide",
"lint": "bunx @biomejs/biome check .",
"check": "bunx tsc -p ."
},
@ -69,7 +70,8 @@
"@tailwindcss/forms": "^0.5.9",
"@types/html-to-text": "^9.0.4",
"typescript": "^5.7.2",
"vue-tsc": "^2.1.10"
"vue-tsc": "^2.1.10",
"@inlang/paraglide-js": "1.11.3"
},
"trustedDependencies": [
"@biomejs/biome",

1
project.inlang/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
cache

View file

@ -0,0 +1 @@
9d8c2839bac9b12c091cd75e30f73bf4570b61f052e0bae7af076988269f44ed

View file

@ -0,0 +1,15 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"sourceLanguageTag": "en",
"languageTags": ["en", "fr"],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{languageTag}.json"
}
}

View file

@ -4,6 +4,7 @@
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"allowImportingTsExtensions": true
"allowImportingTsExtensions": true,
"allowJs": true
}
}