add localisation support
currently only en_GB (TODO: dynamic language pack imports)
This commit is contained in:
		
							parent
							
								
									970590497f
								
							
						
					
					
						commit
						e326ac858e
					
				
					 17 changed files with 263 additions and 90 deletions
				
			
		
							
								
								
									
										88
									
								
								src/lang/en_GB.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/lang/en_GB.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | ||||||
|  | { | ||||||
|  |     "compose_placeholders": [ | ||||||
|  |         "What's cooking, $1?", | ||||||
|  |         "Speak your mind!", | ||||||
|  |         "Federate something...", | ||||||
|  |         "I sure love posting!", | ||||||
|  |         "Another day, another $1 post!" | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     "login": { | ||||||
|  |         "welcome": "Welcome, fediverse user!", | ||||||
|  |         "enter_domain": "Please enter your server domain to log in.", | ||||||
|  |         "experimental": "Please note this is\n<strong><em>extremely experimental software</em></strong>;\nthings are likely to break!\n<br>\nIf that's all cool with you, welcome aboard!", | ||||||
|  |         "button": "Log in", | ||||||
|  |         "error": { | ||||||
|  |             "no_domain": "Please enter an server domain.", | ||||||
|  |             "connection_failed": "Failed to connect to the server.\nCheck the browser console for details!", | ||||||
|  |             "create_app": "Failed to create an application for this server." | ||||||
|  |         }, | ||||||
|  |         "made_with_tagline": "made with ❤ by <a href=\"https://bliss.town\">bliss town</a>" | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     "navigation": { | ||||||
|  |         "timeline": "Timeline", | ||||||
|  |         "notifications": "Notifications", | ||||||
|  |         "explore": "Explore", | ||||||
|  |         "lists": "Lists", | ||||||
|  | 
 | ||||||
|  |         "favourites": "Favourites", | ||||||
|  |         "bookmarks": "Bookmarks", | ||||||
|  |         "hashtags": "Hashtags", | ||||||
|  | 
 | ||||||
|  |         "profile_information": "Profile information", | ||||||
|  |         "settings": "Settings", | ||||||
|  |         "log_out": "Log out" | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     "timeline": { | ||||||
|  |         "home": "Home", | ||||||
|  |         "local": "Local", | ||||||
|  |         "federated": "Federated", | ||||||
|  |         "fetching": "getting the feed..." | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     "notification": { | ||||||
|  |         "and_others": "and <strong>%1</strong> others", | ||||||
|  |         "mention": "%1 mentioned you.", | ||||||
|  |         "reblog": "%1 boosted your post.", | ||||||
|  |         "reaction": "%1 reacted to your post.", | ||||||
|  |         "follow": "%1 followed you.", | ||||||
|  |         "follow_request": "%1 requested to follow you.", | ||||||
|  |         "favourite": "%1 favourited your post.", | ||||||
|  |         "poll": "%1's poll as ended.", | ||||||
|  |         "update": "%1 updated their post.", | ||||||
|  |         "default": "%1 poked you!", | ||||||
|  |         "fetching": "fetching notifications..." | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     "post": { | ||||||
|  |         "time": "%1 ago", | ||||||
|  |         "boosted": "%1 boosted this post.", | ||||||
|  |         "actions": { | ||||||
|  |             "reply": "Reply", | ||||||
|  |             "boost": "Boost", | ||||||
|  |             "favourite": "Favourite", | ||||||
|  |             "quote": "Quote", | ||||||
|  |             "react": "React", | ||||||
|  |             "more": "More", | ||||||
|  |             "delete": "Delete" | ||||||
|  |         }, | ||||||
|  |         "warning": { | ||||||
|  |             "show": "(click to reveal)", | ||||||
|  |             "hide": "(click to hide)" | ||||||
|  |         }, | ||||||
|  |         "visibility": { | ||||||
|  |             "public": "public", | ||||||
|  |             "unlisted": "unlisted", | ||||||
|  |             "private": "private", | ||||||
|  |             "direct": "direct" | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     "compose": "Post", | ||||||
|  |     "search": "Search", | ||||||
|  | 
 | ||||||
|  |     "source": "source", | ||||||
|  |     "issues": "issues" | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								src/lib/lang.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/lib/lang.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | ||||||
|  | import * as en_GB from '@cf/lang/en_GB.json'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @param {string} lang IETH language tag (i.e. en_GB) | ||||||
|  |  * @returns Map<string, string | string[]> | ||||||
|  |  */ | ||||||
|  | export default function init(lang) { | ||||||
|  |     let i18n = new Object(); | ||||||
|  |     let language; | ||||||
|  | 
 | ||||||
|  |     // TODO: dynamic imports seem to fail here; it can't find the file.
 | ||||||
|  |     // try {
 | ||||||
|  |     //     language = import(`../lang/${lang}.json`);
 | ||||||
|  |     // } catch (error) {
 | ||||||
|  |     //     throw error;
 | ||||||
|  |     // }
 | ||||||
|  | 
 | ||||||
|  |     language = en_GB; | ||||||
|  | 
 | ||||||
|  |     i18n.lang = language; | ||||||
|  |     i18n.lang_code = lang; | ||||||
|  |     i18n.string = function(/* @type string */ key) { | ||||||
|  |         const tokens = key.split('.'); | ||||||
|  |          | ||||||
|  |         let i = 0; | ||||||
|  |         let token = tokens[i]; | ||||||
|  |         let res = this.lang; | ||||||
|  |         while (true) { | ||||||
|  |             res = res[token]; | ||||||
|  |             if (res === undefined) { | ||||||
|  |                 console.warn(`${key} not found for language ${this.lang_code}`); | ||||||
|  |                 return key; | ||||||
|  |             } | ||||||
|  |             if (typeof res === 'string' || res instanceof String) | ||||||
|  |                 return res; | ||||||
|  |             i++; | ||||||
|  |             token = tokens[i]; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     i18n.stringArray = function(/* @type string */ key) { | ||||||
|  |         const tokens = key.split('.'); | ||||||
|  |          | ||||||
|  |         let i = 0; | ||||||
|  |         let token = tokens[i]; | ||||||
|  |         let res = this.lang; | ||||||
|  |         while (true) { | ||||||
|  |             res = res[token]; | ||||||
|  |             if (res === undefined) { | ||||||
|  |                 console.warn(`${key} not found for language ${this.lang_code}`); | ||||||
|  |                 return key; | ||||||
|  |             } | ||||||
|  |             if (Array.isArray(res)) | ||||||
|  |                 return res; | ||||||
|  |             i++; | ||||||
|  |             token = tokens[i]; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return i18n; | ||||||
|  | } | ||||||
|  | @ -1,3 +1,6 @@ | ||||||
|  | import Lang from '$lib/lang.js'; | ||||||
|  | const lang = Lang('en_GB'); | ||||||
|  | 
 | ||||||
| const denoms = [ | const denoms = [ | ||||||
|     { unit: 's', min: 0 }, |     { unit: 's', min: 0 }, | ||||||
|     { unit: 'm', min: 60 }, |     { unit: 'm', min: 60 }, | ||||||
|  | @ -18,6 +21,6 @@ export function shorthand(date) { | ||||||
|         unit = denoms[index].unit; |         unit = denoms[index].unit; | ||||||
|     } |     } | ||||||
|     if (value > 0) |     if (value > 0) | ||||||
|         return Math.floor(value) + unit + " ago"; |         return lang.string('post.time').replaceAll('%1', Math.floor(value) + unit); | ||||||
|     return "in " + Math.floor(value) + unit; |     return "in " + Math.floor(value) + unit; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
|     import { timeline } from '$lib/timeline.js'; |     import { timeline } from '$lib/timeline.js'; | ||||||
|     import { createEventDispatcher } from 'svelte'; |     import { createEventDispatcher } from 'svelte'; | ||||||
|     import { playSound } from '$lib/sound'; |     import { playSound } from '$lib/sound'; | ||||||
|  |     import Lang from '$lib/lang.js' | ||||||
| 
 | 
 | ||||||
|     import Button from '@cf/ui/Button.svelte'; |     import Button from '@cf/ui/Button.svelte'; | ||||||
|     import PostIcon from '@cf/icons/post.svg'; |     import PostIcon from '@cf/icons/post.svg'; | ||||||
|  | @ -19,6 +20,8 @@ | ||||||
|     import FollowersVisIcon from '@cf/icons/followers.svg'; |     import FollowersVisIcon from '@cf/icons/followers.svg'; | ||||||
|     import PrivateVisIcon from '@cf/icons/dm.svg'; |     import PrivateVisIcon from '@cf/icons/dm.svg'; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
|  | 
 | ||||||
|     export let reply_id; |     export let reply_id; | ||||||
| 
 | 
 | ||||||
|     let content_warning = "" |     let content_warning = "" | ||||||
|  | @ -27,15 +30,9 @@ | ||||||
|     let show_cw = false; |     let show_cw = false; | ||||||
|     let visibility = "Public"; |     let visibility = "Public"; | ||||||
| 
 | 
 | ||||||
|     const placeholders = [ |     const placeholders = lang.stringArray('compose_placeholders'); | ||||||
|         "What's cooking, $1?", |     let placeholder = Array.isArray(placeholders) ? placeholders[Math.floor(placeholders.length * Math.random())] | ||||||
|         "Speak your mind!", |         .replaceAll("$1", $account.username) : placeholders; | ||||||
|         "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 dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,9 +3,12 @@ | ||||||
|     import { server, createServer } from '$lib/client/server.js'; |     import { server, createServer } from '$lib/client/server.js'; | ||||||
|     import { app } from '$lib/client/app.js'; |     import { app } from '$lib/client/app.js'; | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
|  |     import Lang from '$lib/lang.js'; | ||||||
| 
 | 
 | ||||||
|     import Logo from '$lib/../img/campfire-logo.svg'; |     import Logo from '$lib/../img/campfire-logo.svg'; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
|  | 
 | ||||||
|     let display_error = false; |     let display_error = false; | ||||||
|     let logging_in = false; |     let logging_in = false; | ||||||
| 
 | 
 | ||||||
|  | @ -17,21 +20,21 @@ | ||||||
|         const host = event.target.host.value; |         const host = event.target.host.value; | ||||||
| 
 | 
 | ||||||
|         if (!host || host === "") { |         if (!host || host === "") { | ||||||
|             display_error = "Please enter an server domain."; |             display_error = lang.string('login.error.no_domain'); | ||||||
|             logging_in = false; |             logging_in = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         server.set(await createServer(host)); |         server.set(await createServer(host)); | ||||||
|         if (!get(server)) { |         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; |             logging_in = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         app.set(await api.createApp(get(server).host)); |         app.set(await api.createApp(get(server).host)); | ||||||
|         if (!get(app)) { |         if (!get(app)) { | ||||||
|             display_error = "Failed to create an application for this server." |             display_error = lang.string('login.error.create_app'); | ||||||
|             logging_in = false; |             logging_in = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -44,8 +47,8 @@ | ||||||
|     <div class="app-logo"> |     <div class="app-logo"> | ||||||
|         <Logo /> |         <Logo /> | ||||||
|     </div> |     </div> | ||||||
|     <p>Welcome, fediverse user!</p> |     <p>{lang.string('login.welcome')}</p> | ||||||
|     <p>Please enter your server domain to log in.</p> |     <p>{lang.string('login.enter_domain')}</p> | ||||||
|     <div class="input-wrapper"> |     <div class="input-wrapper"> | ||||||
|         <input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}> |         <input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}> | ||||||
|         {#if display_error} |         {#if display_error} | ||||||
|  | @ -53,16 +56,10 @@ | ||||||
|         {/if} |         {/if} | ||||||
|     </div> |     </div> | ||||||
|     <br> |     <br> | ||||||
|     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button> |     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>{lang.string('login.button')}</button> | ||||||
|     <p><small> |     <p><small>{@html lang.string('login.experimental')}</small></p> | ||||||
|         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> |  | ||||||
| 
 | 
 | ||||||
|     <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> | </form> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import { createEventDispatcher } from 'svelte'; |     import { createEventDispatcher } from 'svelte'; | ||||||
|     import { notifications, unread_notif_count } from '$lib/notifications.js'; |     import { notifications, unread_notif_count } from '$lib/notifications.js'; | ||||||
|  |     import Lang from '$lib/lang.js'; | ||||||
| 
 | 
 | ||||||
|     import Logo from '$lib/../img/campfire-logo.svg'; |     import Logo from '$lib/../img/campfire-logo.svg'; | ||||||
|     import Button from './Button.svelte'; |     import Button from './Button.svelte'; | ||||||
|  | @ -27,6 +28,7 @@ | ||||||
|     import LogoutIcon from '../../img/icons/logout.svg'; |     import LogoutIcon from '../../img/icons/logout.svg'; | ||||||
| 
 | 
 | ||||||
|     const VERSION = APP_VERSION; |     const VERSION = APP_VERSION; | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
| 
 | 
 | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|  | @ -95,7 +97,7 @@ | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <TimelineIcon/> |                 <TimelineIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Timeline |             {lang.string('navigation.timeline')} | ||||||
|         </Button> |         </Button> | ||||||
|         <Button label="Notifications" |         <Button label="Notifications" | ||||||
|                 on:click={() => handle_btn("notifications")} |                 on:click={() => handle_btn("notifications")} | ||||||
|  | @ -103,7 +105,7 @@ | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <NotificationsIcon/> |                 <NotificationsIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Notifications |             {lang.string('navigation.notifications')} | ||||||
|             {#if $unread_notif_count} |             {#if $unread_notif_count} | ||||||
|                 <span class="notification-count"> |                 <span class="notification-count"> | ||||||
|                     {$unread_notif_count <= 99 ? $unread_notif_count : "99+"} |                     {$unread_notif_count <= 99 ? $unread_notif_count : "99+"} | ||||||
|  | @ -114,54 +116,54 @@ | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <ExploreIcon height="auto"/> |                 <ExploreIcon height="auto"/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Explore |             {lang.string('navigation.explore')} | ||||||
|         </Button> |         </Button> | ||||||
|         <Button label="Lists" disabled> |         <Button label="Lists" disabled> | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <ListIcon/> |                 <ListIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Lists |             {lang.string('navigation.lists')} | ||||||
|         </Button> |         </Button> | ||||||
| 
 | 
 | ||||||
|         <div class="flex-row"> |         <div class="flex-row"> | ||||||
|             <Button centered label="Favourites" disabled> |             <Button centered label="{lang.string('navigation.favourites')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <FavouritesIcon/> |                     <FavouritesIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </Button> | ||||||
|             <Button centered label="Bookmarks" disabled> |             <Button centered label="{lang.string('navigation.bookmarks')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <BookmarkIcon/> |                     <BookmarkIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </Button> | ||||||
|             <Button centered label="Hashtags" disabled> |             <Button centered label="{lang.string('navigation.hashtags')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <HashtagIcon/> |                     <HashtagIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </Button> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <Button filled label="Post" on:click={() => dispatch("compose")}> |         <Button filled label="{lang.string('compose')}" on:click={() => dispatch("compose")}> | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <PostIcon/> |                 <PostIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Post |             {lang.string('compose')} | ||||||
|         </Button> |         </Button> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <div id="account-items"> |     <div id="account-items"> | ||||||
|         <div class="flex-row"> |         <div class="flex-row"> | ||||||
|             <Button centered label="Profile information" disabled> |             <Button centered label="{lang.string('navigation.profile_information')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <InfoIcon/> |                     <InfoIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </Button> | ||||||
|             <Button centered label="Settings" disabled> |             <Button centered label="{lang.string('navigation.settings')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <SettingsIcon/> |                     <SettingsIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </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"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <LogoutIcon/> |                     <LogoutIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|  | @ -184,8 +186,8 @@ | ||||||
|         campfire v{VERSION} |         campfire v{VERSION} | ||||||
|         <br> |         <br> | ||||||
|         <ul> |         <ul> | ||||||
|             <li><a href="https://git.arimelody.me/blisstown/campfire">source</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">issues</a></li> |             <li><a href="https://codeberg.org/arimelody/campfire/issues">{lang.string('issues')}</a></li> | ||||||
|         </ul> |         </ul> | ||||||
|     </span> |     </span> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -9,10 +9,13 @@ | ||||||
|     // import QuoteIcon from '$lib/../img/icons/quote.svg'; |     // import QuoteIcon from '$lib/../img/icons/quote.svg'; | ||||||
|     import ReactionBar from '$lib/ui/post/ReactionBar.svelte'; |     import ReactionBar from '$lib/ui/post/ReactionBar.svelte'; | ||||||
|     import ActionBar from '$lib/ui/post/ActionBar.svelte'; |     import ActionBar from '$lib/ui/post/ActionBar.svelte'; | ||||||
|  |     import Lang from '$lib/lang.js'; | ||||||
|  | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
| 
 | 
 | ||||||
|     let mention = (accounts) => { |     let mention = (accounts) => { | ||||||
|         let res = `<a href=${account.url}>${account.rich_name}</a>`; |         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; |         return res; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | @ -20,23 +23,23 @@ | ||||||
|     let activity_text = function (type) { |     let activity_text = function (type) { | ||||||
|         switch (type) { |         switch (type) { | ||||||
|             case "mention": |             case "mention": | ||||||
|                 return `%1 mentioned you.`; |                 return lang.string('notification.mention'); | ||||||
|             case "reblog": |             case "reblog": | ||||||
|                 return `%1 boosted your post.`; |                 return lang.string('notification.reblog'); | ||||||
|             case "reaction": |             case "reaction": | ||||||
|                 return `%1 reacted to your post.`; |                 return lang.string('notification.reaction'); | ||||||
|             case "follow": |             case "follow": | ||||||
|                 return `%1 followed you.`; |                 return lang.string('notification.follow'); | ||||||
|             case "follow_request": |             case "follow_request": | ||||||
|                 return `%1 requested to follow you.`; |                 return lang.string('notification.follow.request'); | ||||||
|             case "favourite": |             case "favourite": | ||||||
|                 return `%1 favourited your post.`; |                 return lang.string('notification.favourite'); | ||||||
|             case "poll": |             case "poll": | ||||||
|                 return `%1's poll as ended.`; |                 return lang.string('notification.poll'); | ||||||
|             case "update": |             case "update": | ||||||
|                 return `%1 updated their post.`; |                 return lang.string('notification.update'); | ||||||
|             default: |             default: | ||||||
|                 return `%1 poked you!`; |                 return lang.string('notification.default'); | ||||||
|         } |         } | ||||||
|     }(data.type); |     }(data.type); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,11 @@ | ||||||
|  | <script> | ||||||
|  |     import Lang from '$lib/lang.js'; | ||||||
|  | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <div id="widgets"> | <div id="widgets"> | ||||||
|     <input type="text" id="search" placeholder="Search"> |     <input type="text" id="search" placeholder="{lang.string('search')}"> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
|     import { timeline } from '$lib/timeline'; |     import { timeline } from '$lib/timeline'; | ||||||
|     import { parseReactions } from '$lib/post'; |     import { parseReactions } from '$lib/post'; | ||||||
|     import { playSound } from '$lib/sound'; |     import { playSound } from '$lib/sound'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
| 
 | 
 | ||||||
|     import ActionButton from './ActionButton.svelte'; |     import ActionButton from './ActionButton.svelte'; | ||||||
| 
 | 
 | ||||||
|  | @ -19,6 +20,8 @@ | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
|  | 
 | ||||||
|     async function toggleBoost() { |     async function toggleBoost() { | ||||||
|         if (!$app || !$app.token) return; |         if (!$app || !$app.token) return; | ||||||
| 
 | 
 | ||||||
|  | @ -74,29 +77,29 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-actions" aria-label="Post actions" role="toolbar" tabindex="0" on:mouseup|stopPropagation on:keydown|stopPropagation> | <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/> |         <ReplyIcon/> | ||||||
|     </ActionButton> |     </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/> |         <RepostIcon/> | ||||||
|         <svelte:fragment slot="activeIcon"> |         <svelte:fragment slot="activeIcon"> | ||||||
|             <RepostIcon/> |             <RepostIcon/> | ||||||
|         </svelte:fragment> |         </svelte:fragment> | ||||||
|     </ActionButton> |     </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/> |         <FavouriteIcon/> | ||||||
|         <svelte:fragment slot="activeIcon"> |         <svelte:fragment slot="activeIcon"> | ||||||
|             <FavouriteIconFill/> |             <FavouriteIconFill/> | ||||||
|         </svelte:fragment> |         </svelte:fragment> | ||||||
|     </ActionButton> |     </ActionButton> | ||||||
|     <ActionButton type="quote" label="Quote" disabled> |     <ActionButton type="quote" label="{lang.string('post.actions.quote')}" disabled> | ||||||
|         <QuoteIcon/> |         <QuoteIcon/> | ||||||
|     </ActionButton> |     </ActionButton> | ||||||
|     <ActionButton type="more" label="More" disabled> |     <ActionButton type="more" label="{lang.string('post.actions.more')}" disabled> | ||||||
|         <MoreIcon/> |         <MoreIcon/> | ||||||
|     </ActionButton> |     </ActionButton> | ||||||
|     {#if $account && post.account.id === $account.id} |     {#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/> |             <DeleteIcon/> | ||||||
|         </ActionButton> |         </ActionButton> | ||||||
|     {/if} |     {/if} | ||||||
|  |  | ||||||
|  | @ -1,25 +1,29 @@ | ||||||
| <script> | <script> | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
|  | 
 | ||||||
|     export let post; |     export let post; | ||||||
| 
 | 
 | ||||||
|     let open_warned = false; |     const lang = Lang('en_GB'); | ||||||
|  | 
 | ||||||
|  |     let open = false; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-body"> | <div class="post-body"> | ||||||
|     {#if post.warning} |     {#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> |         <strong> | ||||||
|             {post.warning} |             {post.warning} | ||||||
|             <span class="warning-instructions"> |             <span class="instructions"> | ||||||
|                 {#if !open_warned} |                 {#if !open} | ||||||
|                     (click to reveal) |                     {lang.string('post.warning.show')} | ||||||
|                 {:else} |                 {:else} | ||||||
|                     (click to hide) |                     {lang.string('post.warning.hide')} | ||||||
|                 {/if} |                 {/if} | ||||||
|             </span> |             </span> | ||||||
|         </strong> |         </strong> | ||||||
|         </button> |         </button> | ||||||
|     {/if} |     {/if} | ||||||
|     {#if !post.warning || open_warned} |     {#if !post.warning || open} | ||||||
|         {#if post.rich_text} |         {#if post.rich_text} | ||||||
|             <span class="post-text">{@html post.rich_text}</span> |             <span class="post-text">{@html post.rich_text}</span> | ||||||
|         {:else if post.html} |         {:else if post.html} | ||||||
|  | @ -78,7 +82,7 @@ | ||||||
|         box-shadow: 0 0 8px var(--warn-bg); |         box-shadow: 0 0 8px var(--warn-bg); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-warning .warning-instructions { |     .post-warning .instructions { | ||||||
|         font-weight: normal; |         font-weight: normal; | ||||||
|         opacity: .5; |         opacity: .5; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| <script> | <script> | ||||||
|     import { shorthand as short_time } from '$lib/time.js'; |     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 post; | ||||||
| 
 | 
 | ||||||
|  | @ -9,10 +12,10 @@ | ||||||
| <div class="post-context"> | <div class="post-context"> | ||||||
|     <span class="post-context-icon">🔁</span> |     <span class="post-context-icon">🔁</span> | ||||||
|     <span class="post-context-action"> |     <span class="post-context-action"> | ||||||
|         <a href={post.account.url} target="_blank"><span class="name"> |         { @html | ||||||
|                 {@html post.account.rich_name}</span> |         lang.string('post.boosted').replaceAll('%1', | ||||||
|         </a> |         `<a href={${post.account.url}} target="_blank"><span class="name">${post.account.rich_name}</span></a>`) | ||||||
|         boosted this post. |         } | ||||||
|     </span> |     </span> | ||||||
|     <span class="post-context-time"> |     <span class="post-context-time"> | ||||||
|         <time title="{time_string}">{short_time(post.created_at)}</time> |         <time title="{time_string}">{short_time(post.created_at)}</time> | ||||||
|  | @ -41,8 +44,8 @@ | ||||||
|         margin-right: 4px; |         margin-right: 4px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-context a, |     :global(.post-context a), | ||||||
|     .post-context a:visited { |     :global(.post-context a:visited) { | ||||||
|         color: inherit; |         color: inherit; | ||||||
|         text-decoration: none; |         text-decoration: none; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| <script> | <script> | ||||||
|     import { shorthand as short_time } from '$lib/time.js'; |     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 post; | ||||||
|     export let reply = undefined; |     export let reply = undefined; | ||||||
|  | @ -19,10 +22,8 @@ | ||||||
|         <div class="post-info" on:mouseup|stopPropagation> |         <div class="post-info" on:mouseup|stopPropagation> | ||||||
|             <a href={post.url} target="_blank" class="created-at"> |             <a href={post.url} target="_blank" class="created-at"> | ||||||
|                 <time title={time_string}>{short_time(post.created_at)}</time> |                 <time title={time_string}>{short_time(post.created_at)}</time> | ||||||
|                 {#if post.visibility !== "public"} |  | ||||||
|                 <br> |                 <br> | ||||||
|                     <span class="post-visibility">{post.visibility}</span> |                 <span class="post-visibility">{lang.string('post.visibility.' + post.visibility)}</span> | ||||||
|                 {/if} |  | ||||||
|             </a> |             </a> | ||||||
|         </div> |         </div> | ||||||
|     </header> |     </header> | ||||||
|  |  | ||||||
|  | @ -51,11 +51,7 @@ | ||||||
|                 {/if} |                 {/if} | ||||||
|         </ReactionButton> |         </ReactionButton> | ||||||
|     {/each} |     {/each} | ||||||
|     <ReactionButton |     <ReactionButton disabled> | ||||||
|             type="reaction" |  | ||||||
|             title="react" |  | ||||||
|             label="React" |  | ||||||
|             disabled> |  | ||||||
|     <ReactIcon/> |     <ReactIcon/> | ||||||
|     </ReactionButton> |     </ReactionButton> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| <script> | <script> | ||||||
|     import { playSound } from '../../sound.js'; |     import { playSound } from '../../sound.js'; | ||||||
|     import { createEventDispatcher } from 'svelte'; |     import { createEventDispatcher } from 'svelte'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
|  | 
 | ||||||
|     export let type = "react"; |     export let type = "react"; | ||||||
|     export let label = "React"; |     export let label = lang.string('post.actions.react'); | ||||||
|     export let title = label; |     export let title = lang.string('post.actions.react'); | ||||||
|     export let count = 0; |     export let count = 0; | ||||||
|     export let active = false; |     export let active = false; | ||||||
|     export let disabled = false; |     export let disabled = false; | ||||||
|  |  | ||||||
|  | @ -2,11 +2,14 @@ | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import { account } from '$lib/stores/account.js'; |     import { account } from '$lib/stores/account.js'; | ||||||
|     import { timeline, getTimeline } from '$lib/timeline.js'; |     import { timeline, getTimeline } from '$lib/timeline.js'; | ||||||
|  |     import Lang from '$lib/lang.js'; | ||||||
| 
 | 
 | ||||||
|     import LoginForm from '$lib/ui/LoginForm.svelte'; |     import LoginForm from '$lib/ui/LoginForm.svelte'; | ||||||
|     import Button from '$lib/ui/Button.svelte'; |     import Button from '$lib/ui/Button.svelte'; | ||||||
|     import Post from '$lib/ui/post/Post.svelte'; |     import Post from '$lib/ui/post/Post.svelte'; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
|  | 
 | ||||||
|     account.subscribe(account => { |     account.subscribe(account => { | ||||||
|         if (account) getTimeline(); |         if (account) getTimeline(); | ||||||
|     }); |     }); | ||||||
|  | @ -21,18 +24,18 @@ | ||||||
| 
 | 
 | ||||||
| {#if $account} | {#if $account} | ||||||
|     <header> |     <header> | ||||||
|         <h1>Home</h1> |         <h1>{lang.string('timeline.home')}</h1> | ||||||
|         <nav> |         <nav> | ||||||
|             <Button centered active>Home</Button> |             <Button centered active>{lang.string('timeline.home')}</Button> | ||||||
|             <Button centered disabled>Local</Button> |             <Button centered disabled>{lang.string('timeline.local')}</Button> | ||||||
|             <Button centered disabled>Federated</Button> |             <Button centered disabled>{lang.string('timeline.federated')}</Button> | ||||||
|         </nav> |         </nav> | ||||||
|     </header> |     </header> | ||||||
| 
 | 
 | ||||||
|     <div id="feed" role="feed"> |     <div id="feed" role="feed"> | ||||||
|         {#if $timeline.length <= 0} |         {#if $timeline.length <= 0} | ||||||
|             <div class="loading throb"> |             <div class="loading throb"> | ||||||
|                 <span>getting the feed...</span> |                 <span>{lang.string('timeline.fetching')}</span> | ||||||
|             </div> |             </div> | ||||||
|         {/if} |         {/if} | ||||||
|         {#each $timeline as post} |         {#each $timeline as post} | ||||||
|  |  | ||||||
|  | @ -4,6 +4,9 @@ | ||||||
|     import { goto } from '$app/navigation'; |     import { goto } from '$app/navigation'; | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import Notification from '$lib/ui/Notification.svelte'; |     import Notification from '$lib/ui/Notification.svelte'; | ||||||
|  |     import Lang from '$lib/lang.js'; | ||||||
|  | 
 | ||||||
|  |     const lang = Lang('en_GB'); | ||||||
| 
 | 
 | ||||||
|     if (!$account) goto("/"); |     if (!$account) goto("/"); | ||||||
| 
 | 
 | ||||||
|  | @ -31,13 +34,13 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <header> | <header> | ||||||
|     <h1>Notifications</h1> |     <h1>{lang.string('navigation.notifications')}</h1> | ||||||
| </header> | </header> | ||||||
| 
 | 
 | ||||||
| <div class="notifications"> | <div class="notifications"> | ||||||
|     {#if $notifications.length === 0} |     {#if $notifications.length === 0} | ||||||
|         <div class="loading throb"> |         <div class="loading throb"> | ||||||
|             <span>fetching notifications...</span> |             <span>{lang.string('notification.fetching')}</span> | ||||||
|         </div> |         </div> | ||||||
|     {:else} |     {:else} | ||||||
|         {#each $notifications as notif} |         {#each $notifications as notif} | ||||||
|  |  | ||||||
|  | @ -19,9 +19,10 @@ const config = { | ||||||
|             name: child_process.execSync('git rev-parse HEAD').toString().trim() |             name: child_process.execSync('git rev-parse HEAD').toString().trim() | ||||||
|         }, |         }, | ||||||
|         alias: { |         alias: { | ||||||
|             '@cf/ui/*': "./src/lib/ui", |             '@cf/ui/*': './src/lib/ui', | ||||||
|             '@cf/icons/*': "./src/img/icons", |             '@cf/icons/*': './src/img/icons', | ||||||
|             '@cf/store/*': "./src/lib/stores" |             '@cf/store/*': './src/lib/stores', | ||||||
|  |             '@cf/lang/*': './src/lang' | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue