feat: start working on internationalization

This commit is contained in:
duskflower 2025-01-24 20:17:03 +01:00
parent 21660101af
commit a9dda69fef
17 changed files with 466 additions and 60 deletions

View file

@ -5,6 +5,7 @@ import "./app.css";
import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from "@kobalte/core";
import { MetaProvider } from "@solidjs/meta";
import { Portal } from "solid-js/web";
import * as m from "~/paraglide/messages";
import { Callout, CalloutContent, CalloutTitle } from "./components/ui/callout";
import { ModeToggle } from "./components/ui/mode-toggle";
import { dbLoaded } from "./db";
@ -44,10 +45,9 @@ const App: Component = () => {
<Portal>
<div class="fixed inset-0 mx-4 flex flex-col items-center justify-center backdrop-blur-lg">
<Callout variant="error">
Your browser does not support WebAssembly, which is required for this site to work with the
big amount of data a signal backup contains.
{m.grassy_early_hawk_find()}
<br />
Please try a different browser.
{m.simple_round_nuthatch_aim()}
</Callout>
</div>
</Portal>
@ -57,8 +57,7 @@ const App: Component = () => {
fallback={
<Show when={!dbLoaded() && hasCashedData()}>
<Callout variant="default" class="m-4">
There is currently no backup database loaded, but you can watch statistics that have been
cached, meaning only chats you already opened or chats that were preloaded.
{m.home_fun_peacock_prosper()}
<br />
<A
href="/overview"
@ -66,19 +65,18 @@ const App: Component = () => {
umami.track("Watch cached statistics");
}}
>
Watch cached statistics
{m.every_tasty_toad_dream()}
</A>
</Callout>
</Show>
}
>
<Callout variant="warning" class="m-4">
<CalloutTitle>You are watching cached statistics</CalloutTitle>
<CalloutTitle>{m.glad_fun_polecat_trim()}</CalloutTitle>
<CalloutContent>
Currently there is no backup database loaded. You can only watch statistics that have been
cached, meaning only chats you already opened or chats that were preloaded.
{m.curly_broad_insect_grow()}
<br />
<A href="/">Load a backup</A>
<A href="/">{m.noble_muddy_elephant_lead()}</A>
</CalloutContent>
</Callout>
</Show>
@ -86,7 +84,7 @@ const App: Component = () => {
<Suspense>{props.children}</Suspense>
</main>
<footer class="mt-4 flex flex-row justify-end bg-muted p-8">
<A href="/privacy">Privacy policy</A>
<A href="/privacy">{m.few_helpful_kestrel_swim()}</A>
</footer>
</ColorModeProvider>
</>

View file

@ -7,6 +7,7 @@ import { Heading } from "~/components/ui/heading";
import { SELF_ID, dmPartnerRecipientQuery, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db";
import { getNameFromRecipient } from "~/lib/get-name-from-recipient";
import { createMessageStatsSources } from "~/lib/messages";
import * as m from "~/paraglide/messages";
import type { MessageOverview } from "~/types";
import { DmMessagesPerDate } from "./dm-messages-per-date";
import { DmMessagesPerDaytime } from "./dm-messages-per-daytime";
@ -90,53 +91,57 @@ export const DmId: Component<RouteSectionProps> = (props) => {
return (
<>
<Title>Dm with {dmPartner()?.name}</Title>
<Title>
{m.petty_best_bobcat_affirm()} {dmPartner()?.name}
</Title>
<div class="flex flex-col items-center">
<Heading level={1}>DM with {dmPartner()?.name}</Heading>
<Heading level={2}>Chat timeline</Heading>
<Heading level={1}>
{m.petty_best_bobcat_affirm()} {dmPartner()?.name}
</Heading>
<Heading level={2}>{m.legal_inclusive_lionfish_zap()}</Heading>
<Suspense
fallback={
<Flex alignItems="center" justifyContent="center" class="h-64">
<p class="text-4xl">Loading...</p>
<p class="text-4xl">{m.mealy_wacky_toucan_spark()}</p>
</Flex>
}
>
<DmMessagesPerDate dateStats={dmMessageStats()?.date} recipients={recipients()} />
</Suspense>
<DmOverview messages={dmMessagesOverview()} />
<Heading level={2}>Messages per</Heading>
<Heading level={2}>{m.fresh_grand_millipede_twirl()}</Heading>
<Suspense
fallback={
<Flex alignItems="center" justifyContent="center" class="h-64">
<p class="text-4xl">Loading...</p>
<p class="text-4xl">{m.mealy_wacky_toucan_spark()}</p>
</Flex>
}
>
<Grid cols={1} colsMd={2} class="gap-x-16 gap-y-16">
<div>
<Heading level={3}>Person</Heading>
<Heading level={3}>{m.top_brief_sparrow_boost()}</Heading>
<DmMessagesPerRecipient personStats={dmMessageStats()?.person} recipients={recipients()} />
</div>
<div>
<Heading level={3}>Daytime</Heading>
<Heading level={3}>{m.cool_cozy_dingo_mop()}</Heading>
<DmMessagesPerDaytime daytimeStats={dmMessageStats()?.daytime} recipients={recipients()} />
</div>
<div>
<Heading level={3}>Month</Heading>
<Heading level={3}>{m.funny_wise_mink_boil()}</Heading>
<DmMessagesPerMonth monthStats={dmMessageStats()?.month} recipients={recipients()} />
</div>
<div>
<Heading level={3}>Weekday</Heading>
<Heading level={3}>{m.wise_house_bobcat_boil()}</Heading>
<DmMessagesPerWeekday weekdayStats={dmMessageStats()?.weekday} recipients={recipients()} />
</div>
</Grid>
</Suspense>
<Heading level={2}>Word cloud</Heading>
<Heading level={2}>{m.north_green_goat_arise()}</Heading>
<Suspense
fallback={
<Flex alignItems="center" justifyContent="center" class="h-64">
<p class="text-4xl">Loading...</p>
<p class="text-4xl">{m.mealy_wacky_toucan_spark()}</p>
</Flex>
}
>

View file

@ -3,6 +3,7 @@ import { type Component, Show } from "solid-js";
import { Flex } from "~/components/ui/flex";
import { Grid } from "~/components/ui/grid";
import { getDistanceBetweenDatesInDays } from "~/lib/date";
import * as m from "~/paraglide/messages";
import type { MessageOverview } from "~/types";
export const DmOverview: Component<{
@ -29,7 +30,7 @@ export const DmOverview: Component<{
<CalendarArrowDown class="h-8 w-8" />
</Flex>
<Flex flexDirection="col" justifyContent="around" class="flex-1">
<span>Your first message is from</span>
<span>{m.knotty_wild_rat_aim()}</span>
<Show when={dmOverview()}>
{(currentDmOverview) => (
<span class="font-semibold text-2xl">{currentDmOverview().firstMessageDate.toDateString()}</span>
@ -42,7 +43,7 @@ export const DmOverview: Component<{
<CalendarArrowUp class="h-8 w-8" />
</Flex>
<Flex flexDirection="col" justifyContent="around" class="flex-1">
<span>Your last message is from</span>
<span>{m.bald_helpful_millipede_intend()}</span>
<Show when={dmOverview()}>
{(currentDmOverview) => (
<span class="font-semibold text-2xl">{currentDmOverview().lastMessageDate.toDateString()}</span>
@ -55,7 +56,7 @@ export const DmOverview: Component<{
<CalendarClock class="h-8 w-8" />
</Flex>
<Flex flexDirection="col" justifyContent="around" class="flex-1">
<span>You have been chatting for</span>
<span>{m.trite_civil_niklas_laugh()}</span>
<Show when={dmOverview()}>
{(currentDmOverview) => (
<span class="font-semibold text-2xl">
@ -66,7 +67,7 @@ export const DmOverview: Component<{
</span>
)}
</Show>
<span>days</span>
<span>{m.careful_real_deer_buzz()}</span>
</Flex>
</Flex>
<Flex flexDirection="row" justifyContent="evenly" class="bg-pink-200 p-2 text-pink-900">
@ -74,13 +75,13 @@ export const DmOverview: Component<{
<MessagesSquare class="h-8 w-8" />
</Flex>
<Flex flexDirection="col" justifyContent="around" class="flex-1">
<span>You have written</span>
<span>{m.due_gaudy_lark_grip()}</span>
<Show when={dmOverview()}>
{(currentDmOverview) => (
<span class="font-semibold text-2xl">{currentDmOverview().messageCount.toString()}</span>
)}
</Show>
<span>messages</span>
<span>{m.weary_noble_ostrich_cry()}</span>
</Flex>
</Flex>
</Grid>

View file

@ -10,6 +10,7 @@ import { Progress, ProgressLabel, ProgressValueLabel } from "~/components/ui/pro
import { TextField, TextFieldInput, TextFieldLabel } from "~/components/ui/text-field";
import { loadDb } from "~/db";
import { decryptBackup } from "~/lib/backup-decryptor";
import * as m from "~/paraglide/messages";
export const Home: Component<RouteSectionProps> = () => {
const navigate = useNavigate();
@ -83,7 +84,7 @@ export const Home: Component<RouteSectionProps> = () => {
}}
>
<Show when={decryptionProgress() !== undefined}>
<p class="font-bold text-2xl">Decrypting backup</p>
<p class="font-bold text-2xl">{m.polite_brief_tern_promise()}</p>
<Progress
value={decryptionProgress()}
minValue={0}
@ -92,13 +93,13 @@ export const Home: Component<RouteSectionProps> = () => {
class="w-[300px] space-y-1"
>
<div class="flex justify-between">
<ProgressLabel>Decrypting...</ProgressLabel>
<ProgressLabel>{m.awake_best_millipede_dazzle()}</ProgressLabel>
<ProgressValueLabel />
</div>
</Progress>
</Show>
<Show when={totalStatements() !== 0}>
<p class="font-bold text-2xl">Loading database</p>
<p class="font-bold text-2xl">{m.ideal_wise_poodle_leap()}</p>
<Progress
value={executedStatements()}
minValue={0}
@ -107,7 +108,7 @@ export const Home: Component<RouteSectionProps> = () => {
class="w-[300px] space-y-1"
>
<div class="flex justify-between">
<ProgressLabel>Loading...</ProgressLabel>
<ProgressLabel>{m.mealy_wacky_toucan_spark()}</ProgressLabel>
<ProgressValueLabel />
</div>
</Progress>
@ -118,7 +119,7 @@ export const Home: Component<RouteSectionProps> = () => {
<form class="mx-auto flex w-full flex-col gap-y-8 p-8 md:w-fit" onSubmit={onSubmit}>
<Flex flexDirection="row" class="w-full gap-x-2 md:w-sm" alignItems="end">
<TextField onChange={(value) => setPassphrase(value)} class="grow">
<TextFieldLabel>Passphrase</TextFieldLabel>
<TextFieldLabel>{m.big_actual_osprey_jump()}</TextFieldLabel>
<TextFieldInput type={showPassphrase() ? "text" : "password"} />
</TextField>
<Button variant="ghost" onClick={() => setShowPassphrase((oldValue) => !oldValue)}>
@ -143,7 +144,7 @@ export const Home: Component<RouteSectionProps> = () => {
})
}
>
Select backup file
{m.same_heroic_robin_grow()}
</Button>
<span
class="absolute bottom-2"
@ -151,11 +152,11 @@ export const Home: Component<RouteSectionProps> = () => {
"text-muted-foreground": !backupFile(),
}}
>
{backupFile() ? backupFile()?.name : "or drop the file here"}
{backupFile() ? backupFile()?.name : m.neat_real_squid_cure()}
</span>
</Flex>
<Button type="submit" class="max-w-72 self-end md:w-sm">
Decrypt and load backup
{m.green_awake_oryx_aid()}
</Button>
</form>
</>

View file

@ -3,6 +3,7 @@ import type { RouteSectionProps } from "@solidjs/router";
import { type Component, Show, createResource } from "solid-js";
import { SELF_ID, allThreadsOverviewQuery, overallSentMessagesQuery } from "~/db";
import { getNameFromRecipient } from "~/lib/get-name-from-recipient";
import * as m from "~/paraglide/messages";
import { OverviewTable, type RoomOverview } from "./overview-table";
export const Overview: Component<RouteSectionProps> = () => {
@ -36,11 +37,12 @@ export const Overview: Component<RouteSectionProps> = () => {
return (
<>
<Title>Signal statistics overview</Title>
<Title>{m.minor_ideal_chipmunk_slide()}</Title>
<div>
<p>All messages: {allSelfSentMessagesCount()?.messageCount as number}</p>
<Show when={!roomOverview.loading && roomOverview()} fallback="Loading...">
<p>
{m.grassy_tidy_wallaby_explore()} {allSelfSentMessagesCount()?.messageCount as number}
</p>
<Show when={!roomOverview.loading && roomOverview()} fallback={m.mealy_wacky_toucan_spark()}>
{(currentRoomOverview) => <OverviewTable data={currentRoomOverview()} />}
</Show>
</div>

View file

@ -24,6 +24,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~
import { TextField, TextFieldInput } from "~/components/ui/text-field";
import { dbLoaded, threadSentMessagesOverviewQuery } from "~/db";
import { cn } from "~/lib/utils";
import * as m from "~/paraglide/messages";
export interface RoomOverview {
threadId: number;
@ -95,7 +96,7 @@ export const columns = [
props.column.toggleSorting();
}}
>
Name
{m.stout_busy_bumblebee_praise()}
<SortingDisplay sorting={sorting()} class="ml-2 h-4 w-4" activeClass="text-info-foreground" />
</Button>
);
@ -114,17 +115,17 @@ export const columns = [
<Flex flexDirection="row" class="ml-auto gap-2">
<Show when={isArchived}>
<Badge variant="outline" class="ml-auto">
Archived
{m.front_shy_gecko_climb()}
</Badge>
</Show>
<Show when={isGroup}>
<Badge variant="outline" class="ml-auto">
Group
{m.broad_pretty_donkey_burn()}
</Badge>
</Show>
<Show when={isNotAvailable}>
<Badge variant="outline" class="ml-auto">
Not available
{m.every_ornate_tuna_slurp()}
</Badge>
</Show>
</Flex>
@ -146,7 +147,7 @@ export const columns = [
props.column.toggleSorting();
}}
>
Number of messages
{m.bland_tidy_rooster_aid()}
<SortingDisplay sorting={sorting()} class="ml-2 h-4 w-4" activeClass="text-info-foreground" />
</Button>
);
@ -165,7 +166,7 @@ export const columns = [
props.column.toggleSorting();
}}
>
Date of last message
{m.silly_bland_buzzard_glow()}
<SortingDisplay sorting={sorting()} class="ml-2 h-4 w-4" activeClass="text-info-foreground" />
</Button>
);
@ -186,7 +187,7 @@ export const columns = [
cell: (props) => {
return (
<Show when={props.cell.getValue()}>
<Badge>Archived</Badge>
<Badge>{m.front_shy_gecko_climb()}</Badge>
</Show>
);
},
@ -197,7 +198,7 @@ export const columns = [
cell: (props) => {
return (
<Show when={props.cell.getValue()}>
<Badge>Group</Badge>
<Badge>{m.broad_pretty_donkey_burn()}</Badge>
</Show>
);
},
@ -274,7 +275,7 @@ export const OverviewTable = (props: OverviewTableProps) => {
table.getColumn("name")?.setFilterValue(value);
}}
>
<TextFieldInput placeholder="Filter by name..." class="max-w-sm" />
<TextFieldInput placeholder={m.home_white_wren_offer()} class="max-w-sm" />
</TextField>
</div>
<div class="flex items-start space-x-2">
@ -287,7 +288,7 @@ export const OverviewTable = (props: OverviewTableProps) => {
}}
/>
<div class="grid gap-1.5 leading-none">
<Label for="show-archived">Show archived chats</Label>
<Label for="show-archived">{m.light_safe_crow_treasure()}</Label>
</div>
</div>
<div class="flex items-start space-x-2">
@ -300,7 +301,7 @@ export const OverviewTable = (props: OverviewTableProps) => {
}}
/>
<div class="grid gap-1.5 leading-none">
<Label for="show-groups">Show group chats (detailed analysis not implemented)</Label>
<Label for="show-groups">{m.proof_heavy_dingo_bubble()}</Label>
</div>
</div>
</div>

View file

@ -2,11 +2,12 @@ import type { RouteSectionProps } from "@solidjs/router";
import type { Component } from "solid-js";
import { A } from "~/components/ui/A";
import { Heading } from "~/components/ui/heading";
import * as m from "~/paraglide/messages";
export const Privacy: Component<RouteSectionProps> = () => {
return (
<div class="m-auto mb-8 max-w-4xl [&>:is(p,ul)]:mb-4">
<Heading level={1}>Privacy policy</Heading>
<Heading level={1}>{m.few_helpful_kestrel_swim()}</Heading>
<Heading level={2}>Introduction</Heading>
<p>
This project ("signalstats", "I", "my") was built with the intention to not harm the privacy of its users. This