add localisation support

currently only en_GB (TODO: dynamic language pack imports)
This commit is contained in:
ari melody 2025-07-13 18:35:26 +01:00
parent 970590497f
commit e326ac858e
Signed by: ari
GPG key ID: CF99829C92678188
17 changed files with 263 additions and 90 deletions

View file

@ -7,6 +7,7 @@
import { timeline } from '$lib/timeline.js';
import { createEventDispatcher } from 'svelte';
import { playSound } from '$lib/sound';
import Lang from '$lib/lang.js'
import Button from '@cf/ui/Button.svelte';
import PostIcon from '@cf/icons/post.svg';
@ -19,6 +20,8 @@
import FollowersVisIcon from '@cf/icons/followers.svg';
import PrivateVisIcon from '@cf/icons/dm.svg';
const lang = Lang('en_GB');
export let reply_id;
let content_warning = ""
@ -27,15 +30,9 @@
let show_cw = false;
let visibility = "Public";
const placeholders = [
"What's cooking, $1?",
"Speak your mind!",
"Federate something...",
"I sure love posting!",
"Another day, another $1 post!",
];
let placeholder = placeholders[Math.floor(placeholders.length * Math.random())]
.replaceAll("$1", $account.username);
const placeholders = lang.stringArray('compose_placeholders');
let placeholder = Array.isArray(placeholders) ? placeholders[Math.floor(placeholders.length * Math.random())]
.replaceAll("$1", $account.username) : placeholders;
const dispatch = createEventDispatcher();

View file

@ -3,9 +3,12 @@
import { server, createServer } from '$lib/client/server.js';
import { app } from '$lib/client/app.js';
import { get } from 'svelte/store';
import Lang from '$lib/lang.js';
import Logo from '$lib/../img/campfire-logo.svg';
const lang = Lang('en_GB');
let display_error = false;
let logging_in = false;
@ -17,21 +20,21 @@
const host = event.target.host.value;
if (!host || host === "") {
display_error = "Please enter an server domain.";
display_error = lang.string('login.error.no_domain');
logging_in = false;
return;
}
server.set(await createServer(host));
if (!get(server)) {
display_error = "Failed to connect to the server.\nCheck the browser console for details!"
display_error = lang.string('login.error.connection_failed');
logging_in = false;
return;
}
app.set(await api.createApp(get(server).host));
if (!get(app)) {
display_error = "Failed to create an application for this server."
display_error = lang.string('login.error.create_app');
logging_in = false;
return;
}
@ -44,8 +47,8 @@
<div class="app-logo">
<Logo />
</div>
<p>Welcome, fediverse user!</p>
<p>Please enter your server domain to log in.</p>
<p>{lang.string('login.welcome')}</p>
<p>{lang.string('login.enter_domain')}</p>
<div class="input-wrapper">
<input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}>
{#if display_error}
@ -53,16 +56,10 @@
{/if}
</div>
<br>
<button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button>
<p><small>
Please note this is
<strong><em>extremely experimental software</em></strong>;
things are likely to break!
<br>
If that's all cool with you, welcome aboard!
</small></p>
<button type="submit" id="login" class={logging_in ? "disabled" : ""}>{lang.string('login.button')}</button>
<p><small>{@html lang.string('login.experimental')}</small></p>
<p class="form-footer">made with ❤ by <a href="https://bliss.town">bliss town</a>, 2024</p>
<p class="form-footer">{@html lang.string('login.made_with_tagline')}</p>
</form>
<style>

View file

@ -10,6 +10,7 @@
import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import { notifications, unread_notif_count } from '$lib/notifications.js';
import Lang from '$lib/lang.js';
import Logo from '$lib/../img/campfire-logo.svg';
import Button from './Button.svelte';
@ -27,6 +28,7 @@
import LogoutIcon from '../../img/icons/logout.svg';
const VERSION = APP_VERSION;
const lang = Lang('en_GB');
const dispatch = createEventDispatcher();
@ -95,7 +97,7 @@
<svelte:fragment slot="icon">
<TimelineIcon/>
</svelte:fragment>
Timeline
{lang.string('navigation.timeline')}
</Button>
<Button label="Notifications"
on:click={() => handle_btn("notifications")}
@ -103,7 +105,7 @@
<svelte:fragment slot="icon">
<NotificationsIcon/>
</svelte:fragment>
Notifications
{lang.string('navigation.notifications')}
{#if $unread_notif_count}
<span class="notification-count">
{$unread_notif_count <= 99 ? $unread_notif_count : "99+"}
@ -114,54 +116,54 @@
<svelte:fragment slot="icon">
<ExploreIcon height="auto"/>
</svelte:fragment>
Explore
{lang.string('navigation.explore')}
</Button>
<Button label="Lists" disabled>
<svelte:fragment slot="icon">
<ListIcon/>
</svelte:fragment>
Lists
{lang.string('navigation.lists')}
</Button>
<div class="flex-row">
<Button centered label="Favourites" disabled>
<Button centered label="{lang.string('navigation.favourites')}" disabled>
<svelte:fragment slot="icon">
<FavouritesIcon/>
</svelte:fragment>
</Button>
<Button centered label="Bookmarks" disabled>
<Button centered label="{lang.string('navigation.bookmarks')}" disabled>
<svelte:fragment slot="icon">
<BookmarkIcon/>
</svelte:fragment>
</Button>
<Button centered label="Hashtags" disabled>
<Button centered label="{lang.string('navigation.hashtags')}" disabled>
<svelte:fragment slot="icon">
<HashtagIcon/>
</svelte:fragment>
</Button>
</div>
<Button filled label="Post" on:click={() => dispatch("compose")}>
<Button filled label="{lang.string('compose')}" on:click={() => dispatch("compose")}>
<svelte:fragment slot="icon">
<PostIcon/>
</svelte:fragment>
Post
{lang.string('compose')}
</Button>
</div>
<div id="account-items">
<div class="flex-row">
<Button centered label="Profile information" disabled>
<Button centered label="{lang.string('navigation.profile_information')}" disabled>
<svelte:fragment slot="icon">
<InfoIcon/>
</svelte:fragment>
</Button>
<Button centered label="Settings" disabled>
<Button centered label="{lang.string('navigation.settings')}" disabled>
<svelte:fragment slot="icon">
<SettingsIcon/>
</svelte:fragment>
</Button>
<Button centered label="Log out" on:click={() => log_out()}>
<Button centered label="{lang.string('navigation.log_out')}" on:click={() => log_out()}>
<svelte:fragment slot="icon">
<LogoutIcon/>
</svelte:fragment>
@ -184,8 +186,8 @@
campfire v{VERSION}
<br>
<ul>
<li><a href="https://git.arimelody.me/blisstown/campfire">source</a></li>
<li><a href="https://codeberg.org/arimelody/campfire/issues">issues</a></li>
<li><a href="https://git.arimelody.me/blisstown/campfire">{lang.string('source')}</a></li>
<li><a href="https://codeberg.org/arimelody/campfire/issues">{lang.string('issues')}</a></li>
</ul>
</span>
</div>

View file

@ -9,10 +9,13 @@
// import QuoteIcon from '$lib/../img/icons/quote.svg';
import ReactionBar from '$lib/ui/post/ReactionBar.svelte';
import ActionBar from '$lib/ui/post/ActionBar.svelte';
import Lang from '$lib/lang.js';
const lang = Lang('en_GB');
let mention = (accounts) => {
let res = `<a href=${account.url}>${account.rich_name}</a>`;
if (accounts.length > 1) res += ` and <strong>${accounts.length - 1}</strong> others`;
if (accounts.length > 1) res += ' ' + lang.string('notification.and_others').replaceAll('%1', accounts.length - 1);
return res;
};
@ -20,23 +23,23 @@
let activity_text = function (type) {
switch (type) {
case "mention":
return `%1 mentioned you.`;
return lang.string('notification.mention');
case "reblog":
return `%1 boosted your post.`;
return lang.string('notification.reblog');
case "reaction":
return `%1 reacted to your post.`;
return lang.string('notification.reaction');
case "follow":
return `%1 followed you.`;
return lang.string('notification.follow');
case "follow_request":
return `%1 requested to follow you.`;
return lang.string('notification.follow.request');
case "favourite":
return `%1 favourited your post.`;
return lang.string('notification.favourite');
case "poll":
return `%1's poll as ended.`;
return lang.string('notification.poll');
case "update":
return `%1 updated their post.`;
return lang.string('notification.update');
default:
return `%1 poked you!`;
return lang.string('notification.default');
}
}(data.type);

View file

@ -1,5 +1,11 @@
<script>
import Lang from '$lib/lang.js';
const lang = Lang('en_GB');
</script>
<div id="widgets">
<input type="text" id="search" placeholder="Search">
<input type="text" id="search" placeholder="{lang.string('search')}">
</div>
<style>

View file

@ -6,6 +6,7 @@
import { timeline } from '$lib/timeline';
import { parseReactions } from '$lib/post';
import { playSound } from '$lib/sound';
import Lang from '$lib/lang';
import ActionButton from './ActionButton.svelte';
@ -19,6 +20,8 @@
export let post;
const lang = Lang('en_GB');
async function toggleBoost() {
if (!$app || !$app.token) return;
@ -74,29 +77,29 @@
</script>
<div class="post-actions" aria-label="Post actions" role="toolbar" tabindex="0" on:mouseup|stopPropagation on:keydown|stopPropagation>
<ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>
<ActionButton type="reply" label="{lang.string('post.actions.reply')}" bind:count={post.reply_count} sound="post" disabled>
<ReplyIcon/>
</ActionButton>
<ActionButton type="boost" label="Boost" on:click={toggleBoost} bind:active={post.boosted} bind:count={post.boost_count} disabled={!$account}>
<ActionButton type="boost" label="{lang.string('post.actions.boost')}" on:click={toggleBoost} bind:active={post.boosted} bind:count={post.boost_count} disabled={!$account}>
<RepostIcon/>
<svelte:fragment slot="activeIcon">
<RepostIcon/>
</svelte:fragment>
</ActionButton>
<ActionButton type="favourite" label="Favourite" on:click={toggleFavourite} bind:active={post.favourited} bind:count={post.favourite_count} disabled={!$account}>
<ActionButton type="favourite" label="{lang.string('post.actions.favourite')}" on:click={toggleFavourite} bind:active={post.favourited} bind:count={post.favourite_count} disabled={!$account}>
<FavouriteIcon/>
<svelte:fragment slot="activeIcon">
<FavouriteIconFill/>
</svelte:fragment>
</ActionButton>
<ActionButton type="quote" label="Quote" disabled>
<ActionButton type="quote" label="{lang.string('post.actions.quote')}" disabled>
<QuoteIcon/>
</ActionButton>
<ActionButton type="more" label="More" disabled>
<ActionButton type="more" label="{lang.string('post.actions.more')}" disabled>
<MoreIcon/>
</ActionButton>
{#if $account && post.account.id === $account.id}
<ActionButton type="delete" label="Delete" on:click={deletePost}>
<ActionButton type="delete" label="{lang.string('post.actions.delete')}" on:click={deletePost}>
<DeleteIcon/>
</ActionButton>
{/if}

View file

@ -1,25 +1,29 @@
<script>
import Lang from '$lib/lang';
export let post;
let open_warned = false;
const lang = Lang('en_GB');
let open = false;
</script>
<div class="post-body">
{#if post.warning}
<button class="post-warning" on:click|stopPropagation={() => { open_warned = !open_warned }} on:mouseup|stopPropagation>
<button class="post-warning" on:click|stopPropagation={() => { open = !open }} on:mouseup|stopPropagation>
<strong>
{post.warning}
<span class="warning-instructions">
{#if !open_warned}
(click to reveal)
<span class="instructions">
{#if !open}
{lang.string('post.warning.show')}
{:else}
(click to hide)
{lang.string('post.warning.hide')}
{/if}
</span>
</strong>
</button>
{/if}
{#if !post.warning || open_warned}
{#if !post.warning || open}
{#if post.rich_text}
<span class="post-text">{@html post.rich_text}</span>
{:else if post.html}
@ -78,7 +82,7 @@
box-shadow: 0 0 8px var(--warn-bg);
}
.post-warning .warning-instructions {
.post-warning .instructions {
font-weight: normal;
opacity: .5;
}

View file

@ -1,5 +1,8 @@
<script>
import { shorthand as short_time } from '$lib/time.js';
import Lang from '$lib/lang.js';
const lang = Lang('en_GB');
export let post;
@ -9,10 +12,10 @@
<div class="post-context">
<span class="post-context-icon">🔁</span>
<span class="post-context-action">
<a href={post.account.url} target="_blank"><span class="name">
{@html post.account.rich_name}</span>
</a>
boosted this post.
{ @html
lang.string('post.boosted').replaceAll('%1',
`<a href={${post.account.url}} target="_blank"><span class="name">${post.account.rich_name}</span></a>`)
}
</span>
<span class="post-context-time">
<time title="{time_string}">{short_time(post.created_at)}</time>
@ -41,8 +44,8 @@
margin-right: 4px;
}
.post-context a,
.post-context a:visited {
:global(.post-context a),
:global(.post-context a:visited) {
color: inherit;
text-decoration: none;
}

View file

@ -1,5 +1,8 @@
<script>
import { shorthand as short_time } from '$lib/time.js';
import Lang from '$lib/lang.js';
const lang = Lang('en_GB');
export let post;
export let reply = undefined;
@ -19,10 +22,8 @@
<div class="post-info" on:mouseup|stopPropagation>
<a href={post.url} target="_blank" class="created-at">
<time title={time_string}>{short_time(post.created_at)}</time>
{#if post.visibility !== "public"}
<br>
<span class="post-visibility">{post.visibility}</span>
{/if}
<br>
<span class="post-visibility">{lang.string('post.visibility.' + post.visibility)}</span>
</a>
</div>
</header>

View file

@ -51,11 +51,7 @@
{/if}
</ReactionButton>
{/each}
<ReactionButton
type="reaction"
title="react"
label="React"
disabled>
<ReactionButton disabled>
<ReactIcon/>
</ReactionButton>
</div>

View file

@ -1,11 +1,14 @@
<script>
import { playSound } from '../../sound.js';
import { createEventDispatcher } from 'svelte';
import Lang from '$lib/lang';
const dispatch = createEventDispatcher();
const lang = Lang('en_GB');
export let type = "react";
export let label = "React";
export let title = label;
export let label = lang.string('post.actions.react');
export let title = lang.string('post.actions.react');
export let count = 0;
export let active = false;
export let disabled = false;