Merge branch 'main' of github.com:ayoayco/elk
Some checks are pending
ci / ci (push) Waiting to run
build & push docker container / docker (push) Waiting to run
ci / check-provenance (push) Waiting to run

This commit is contained in:
Ayo Ayco 2025-11-21 15:27:14 +01:00
commit 3f8a381c09
78 changed files with 10778 additions and 10178 deletions

View file

@ -17,12 +17,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
# workaround for npm registry key change # workaround for npm registry key change
# ref. `pnpm@10.1.0` / `pnpm@9.15.4` cannot be installed due to key id mismatch · Issue #612 · nodejs/corepack # ref. `pnpm@10.1.0` / `pnpm@9.15.4` cannot be installed due to key id mismatch · Issue #612 · nodejs/corepack
# - https://github.com/nodejs/corepack/issues/612#issuecomment-2629496091 # - https://github.com/nodejs/corepack/issues/612#issuecomment-2629496091
- run: npm i -g corepack@latest && corepack enable - run: npm i -g corepack@latest && corepack enable
- uses: actions/setup-node@v4.4.0 - uses: actions/setup-node@v6.0.0
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc

View file

@ -16,7 +16,7 @@ jobs:
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Docker meta - name: Docker meta
id: metal id: metal
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5

22
.github/workflows/provenance.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: ci
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
check-provenance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check provenance downgrades
uses: danielroe/provenance-action@41bcc969e579d9e29af08ba44fcbfdf95cee6e6c # v0.1.1
with:
fail-on-provenance-change: true

View file

@ -12,12 +12,12 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set node - name: Set node
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc

View file

@ -19,6 +19,6 @@ jobs:
name: Semantic Pull Request name: Semantic Pull Request
steps: steps:
- name: Validate PR title - name: Validate PR title
uses: amannn/action-semantic-pull-request@v5.5.3 uses: amannn/action-semantic-pull-request@v6.1.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View file

@ -4,6 +4,7 @@ dist
.output .output
.pnpm-store .pnpm-store
.nuxt .nuxt
.data
.env .env
.DS_Store .DS_Store
.idea/ .idea/

4
.npmrc
View file

@ -1,4 +0,0 @@
shamefully-hoist=true
shell-emulator=true
ignore-workspace-root-check=true
package-manager-strict=false

2
.nvmrc
View file

@ -1 +1 @@
22 lts/*

View file

@ -17,13 +17,12 @@ RUN apk add git --no-cache
# Prepare build deps ( ignore postinstall scripts for now ) # Prepare build deps ( ignore postinstall scripts for now )
COPY package.json ./ COPY package.json ./
COPY .npmrc ./
COPY pnpm-lock.yaml ./ COPY pnpm-lock.yaml ./
COPY patches ./patches
RUN pnpm i --frozen-lockfile --ignore-scripts RUN pnpm i --frozen-lockfile --ignore-scripts
# Copy all source files # Copy all source files
COPY . ./ COPY . ./
RUN pnpm nuxt prepare
# Run full install with every postinstall script ( This needs project file ) # Run full install with every postinstall script ( This needs project file )
RUN pnpm i --frozen-lockfile RUN pnpm i --frozen-lockfile

View file

@ -34,7 +34,7 @@ One could put Elk behind popular reverse proxies with SSL Handling like Traefik,
1. got into new source dir: ```cd elk``` 1. got into new source dir: ```cd elk```
1. create local storage directory for settings: ```mkdir elk-storage``` 1. create local storage directory for settings: ```mkdir elk-storage```
1. adjust permissions of storage dir: ```sudo chown 911:911 ./elk-storage``` 1. adjust permissions of storage dir: ```sudo chown 911:911 ./elk-storage```
1. start container: ```docker-compose up --build -d``` 1. start container: ```docker compose up --build -d```
> [!NOTE] > [!NOTE]
> The provided Dockerfile creates a container which will eventually run Elk as non-root user and create a persistent named Docker volume upon first start (if that volume does not yet exist). This volume is always created with root permission. Failing to change the permissions of ```/elk/data``` inside this volume to UID:GID 911 (as specified for Elk in the Dockerfile) will prevent Elk from storing it's config for user accounts. You either have to fix the permission in the created named volume, or mount a directory with the correct permission to ```/elk/data``` into the container. > The provided Dockerfile creates a container which will eventually run Elk as non-root user and create a persistent named Docker volume upon first start (if that volume does not yet exist). This volume is always created with root permission. Failing to change the permissions of ```/elk/data``` inside this volume to UID:GID 911 (as specified for Elk in the Dockerfile) will prevent Elk from storing it's config for user accounts. You either have to fix the permission in the created named volume, or mount a directory with the correct permission to ```/elk/data``` into the container.

19
app/augments.d.ts vendored Normal file
View file

@ -0,0 +1,19 @@
export {}
declare module '#app' {
interface PageMeta {
wideLayout?: boolean
}
interface RuntimeNuxtHooks {
'elk-logo:click': () => void
}
}
declare global {
namespace NodeJS {
interface Process {
mock?: Record<string, any>
}
}
}

View file

@ -30,12 +30,11 @@ const containerClass = computed(() => {
sticky top-0 z-20 sticky top-0 z-20
pt="[env(safe-area-inset-top,0)]" pt="[env(safe-area-inset-top,0)]"
bg="[rgba(var(--rgb-bg-base),0.7)]" bg="[rgba(var(--rgb-bg-base),0.7)]"
class="native:lg:w-[calc(100vw-5rem)] native:xl:w-[calc(135%+(100vw-1200px)/2)]"
:class="{ :class="{
'backdrop-blur': !getPreferences(userSettings, 'optimizeForLowPerformanceDevice'), 'backdrop-blur': !getPreferences(userSettings, 'optimizeForLowPerformanceDevice'),
}" }"
> >
<div flex justify-between gap-2 min-h-53px px5 py1 :class="{ 'xl:hidden': $route.name !== 'tag' }" class="native:xl:flex" border="b base"> <div flex justify-between gap-2 min-h-53px px5 py1 :class="{ 'xl:hidden': $route.name !== 'tag' }" border="b base">
<div flex gap-2 items-center :overflow-hidden="!noOverflowHidden ? '' : false" w-full> <div flex gap-2 items-center :overflow-hidden="!noOverflowHidden ? '' : false" w-full>
<button <button
v-if="backOnSmallScreen || back" v-if="backOnSmallScreen || back"
@ -45,7 +44,7 @@ const containerClass = computed(() => {
> >
<div text-lg i-ri:arrow-left-line class="rtl-flip" /> <div text-lg i-ri:arrow-left-line class="rtl-flip" />
</button> </button>
<div :truncate="!noOverflowHidden ? '' : false" flex w-full data-tauri-drag-region class="native-mac:justify-start native-mac:text-center"> <div :truncate="!noOverflowHidden ? '' : false" flex w-full class="native-mac:justify-start native-mac:text-center">
<slot name="title" /> <slot name="title" />
</div> </div>
<div sm:hidden h-7 w-1px /> <div sm:hidden h-7 w-1px />

View file

@ -13,9 +13,9 @@ watchEffect(() => {
} }
const duration const duration
= days.value * 24 * 60 * 60 = days.value * 24 * 60 * 60
+ hours.value * 60 * 60 + hours.value * 60 * 60
+ minutes.value * 60 + minutes.value * 60
if (duration <= 0) { if (duration <= 0) {
isValid.value = false isValid.value = false

View file

@ -82,7 +82,7 @@ function handleFavouritedBoostedByClose() {
> >
<ModalMediaPreview v-if="isMediaPreviewOpen" @close="closeMediaPreview()" /> <ModalMediaPreview v-if="isMediaPreviewOpen" @close="closeMediaPreview()" />
</ModalDialog> </ModalDialog>
<ModalDialog v-model="isEditHistoryDialogOpen" max-w-125> <ModalDialog v-model="isEditHistoryDialogOpen" :focus-first-element="false" max-w-125>
<StatusEditPreview v-if="statusEdit" :edit="statusEdit" /> <StatusEditPreview v-if="statusEdit" :edit="statusEdit" />
</ModalDialog> </ModalDialog>
<ModalDialog v-model="isCommandPanelOpen" max-w-fit flex> <ModalDialog v-model="isCommandPanelOpen" max-w-fit flex>

View file

@ -10,6 +10,7 @@ const {
closeByMask = true, closeByMask = true,
useVIf = true, useVIf = true,
keepAlive = false, keepAlive = false,
focusFirstElement = true,
} = defineProps<{ } = defineProps<{
// level of depth // level of depth
zIndex?: number zIndex?: number
@ -21,6 +22,8 @@ const {
keepAlive?: boolean keepAlive?: boolean
// The aria-labelledby id for the dialog. // The aria-labelledby id for the dialog.
dialogLabelledBy?: string dialogLabelledBy?: string
// Whether to focus on the first element when the modal opens.
focusFirstElement?: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -45,6 +48,7 @@ const { activate } = useFocusTrap(elDialogRoot, {
escapeDeactivates: true, escapeDeactivates: true,
preventScroll: true, preventScroll: true,
returnFocusOnDeactivate: true, returnFocusOnDeactivate: true,
initialFocus: focusFirstElement ? undefined : false,
}) })
defineExpose({ defineExpose({

View file

@ -18,7 +18,7 @@ router.afterEach(() => {
</script> </script>
<template> <template>
<div flex justify-between sticky top-0 bg-base z-1 py-4 native:py-7 data-tauri-drag-region> <div flex justify-between sticky top-0 bg-base z-1 py-4>
<NuxtLink <NuxtLink
flex items-end gap-3 flex items-end gap-3
py2 px-5 py2 px-5

View file

@ -122,9 +122,9 @@ export const useCommandRegistry = defineStore('command', () => {
const fuse = (lastScope === scope && lastFuse) const fuse = (lastScope === scope && lastFuse)
? lastFuse ? lastFuse
: new Fuse(cmds, { : new Fuse(cmds, {
keys: ['scope', 'name', 'description'], keys: ['scope', 'name', 'description'],
includeScore: true, includeScore: true,
}) })
lastScope = scope lastScope = scope
lastFuse = fuse lastFuse = fuse

View file

@ -67,6 +67,11 @@ const sanitizer = sanitize({
li: { li: {
value: keep, value: keep,
}, },
// Hollo supports <ruby> tags
// https://github.com/fedify-dev/hollo/blob/80e7184aa805f579be8712ff9231be655343c661/src/xss.ts#L92-L94
ruby: {},
rp: {},
rt: {},
}) })
/** /**

View file

@ -17,6 +17,7 @@ export const shikiParser: Parser = (options) => {
return promise ?? [] return promise ?? []
if (!parser) if (!parser)
// @ts-expect-error will be fixed when shiki upgrades
parser = createParser(highlighter) parser = createParser(highlighter)
return parser(options) return parser(options)

View file

@ -16,9 +16,9 @@ const instance = instanceStorage.value[currentServer.value]
</script> </script>
<template> <template>
<div h-full :data-mode="isHydrated && isGrayscale ? 'grayscale' : ''" data-tauri-drag-region> <div h-full :data-mode="isHydrated && isGrayscale ? 'grayscale' : ''">
<main flex w-full mxa lg:max-w-80rem class="native:grid native:sm:grid-cols-[auto_1fr] native:lg:grid-cols-[auto_minmax(600px,2fr)_1fr]"> <main flex w-full mxa lg:max-w-80rem>
<aside class="native:w-auto w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 zen-hide" hidden sm:flex justify-end xl:me-4 native:me-0 relative> <aside class="w-1/8 md:w-1/6 lg:w-1/5 xl:w-1/4 zen-hide" hidden sm:flex justify-end xl:me-4 relative>
<div sticky top-0 w-20 xl:w-100 h-100dvh flex="~ col" lt-xl-items-center> <div sticky top-0 w-20 xl:w-100 h-100dvh flex="~ col" lt-xl-items-center>
<slot name="left"> <slot name="left">
<div flex="~ col" overflow-y-auto justify-between h-full max-w-full overflow-x-hidden> <div flex="~ col" overflow-y-auto justify-between h-full max-w-full overflow-x-hidden>
@ -60,7 +60,7 @@ const instance = instanceStorage.value[currentServer.value]
<NavBottom v-if="isHydrated" sm:hidden /> <NavBottom v-if="isHydrated" sm:hidden />
</div> </div>
</div> </div>
<aside v-if="isHydrated && !wideLayout" class="hidden lg:w-1/5 xl:w-1/4 sm:none xl:block native:w-full zen-hide"> <aside v-if="isHydrated && !wideLayout" class="hidden lg:w-1/5 xl:w-1/4 sm:none xl:block zen-hide">
<div sticky top-0 h-100dvh flex="~ col" gap-2 py3 ms-2> <div sticky top-0 h-100dvh flex="~ col" gap-2 py3 ms-2>
<slot name="right"> <slot name="right">
<SearchWidget mt-4 mx-1 hidden xl:block /> <SearchWidget mt-4 mx-1 hidden xl:block />

View file

@ -19,11 +19,11 @@ export default defineNuxtPlugin(async (nuxt) => {
if (!supportLanguages.includes(lang.value)) if (!supportLanguages.includes(lang.value))
userSettings.value.language = getDefaultLanguage(supportLanguages) userSettings.value.language = getDefaultLanguage(supportLanguages)
if (lang.value !== i18n.locale) if (lang.value !== i18n.locale.value)
await setLocale(userSettings.value.language as Locale) await setLocale(userSettings.value.language as Locale)
watch([lang, isHydrated], () => { watch([lang, isHydrated], () => {
if (isHydrated.value && lang.value !== i18n.locale) if (isHydrated.value && lang.value !== i18n.locale.value)
setLocale(lang.value) setLocale(lang.value)
}, { immediate: true }) }, { immediate: true })
} }

View file

@ -5,7 +5,7 @@ export { version } from '../package.json'
/** /**
* Environment variable `PULL_REQUEST` provided by Netlify. * Environment variable `PULL_REQUEST` provided by Netlify.
* @see {@link https://docs.netlify.com/configure-builds/environment-variables/#git-metadata} * @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#git-metadata}
* *
* Whether triggered by a GitHub PR * Whether triggered by a GitHub PR
*/ */
@ -13,7 +13,7 @@ export const isPR = process.env.PULL_REQUEST === 'true'
/** /**
* Environment variable `BRANCH` provided by Netlify. * Environment variable `BRANCH` provided by Netlify.
* @see {@link https://docs.netlify.com/configure-builds/environment-variables/#git-metadata} * @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#git-metadata}
* *
* Git branch * Git branch
*/ */
@ -21,7 +21,7 @@ export const gitBranch = process.env.BRANCH
/** /**
* Environment variable `CONTEXT` provided by Netlify. * Environment variable `CONTEXT` provided by Netlify.
* @see {@link https://docs.netlify.com/configure-builds/environment-variables/#build-metadata} * @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#build-metadata}
* *
* Whether triggered by PR, `deploy-preview` or `dev`. * Whether triggered by PR, `deploy-preview` or `dev`.
*/ */

View file

@ -75,13 +75,11 @@ export const countryLocaleVariants: Record<string, (LocaleObjectData & { country
const locales: LocaleObjectData[] = [ const locales: LocaleObjectData[] = [
{ {
// @ts-expect-error en used as placeholder
code: 'en', code: 'en',
file: 'en.json', file: 'en.json',
name: 'English', name: 'English',
}, },
({ {
// @ts-expect-error ar used as placeholder
code: 'ar', code: 'ar',
file: 'ar.json', file: 'ar.json',
name: 'العربية', name: 'العربية',
@ -90,8 +88,8 @@ const locales: LocaleObjectData[] = [
const name = new Intl.PluralRules('ar-EG').select(choice) const name = new Intl.PluralRules('ar-EG').select(choice)
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name] return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
}, },
} satisfies LocaleObjectData), } satisfies LocaleObjectData,
({ {
code: 'ckb', code: 'ckb',
file: 'ckb.json', file: 'ckb.json',
name: 'کوردیی ناوەندی', name: 'کوردیی ناوەندی',
@ -100,8 +98,8 @@ const locales: LocaleObjectData[] = [
const name = new Intl.PluralRules('ckb').select(choice) const name = new Intl.PluralRules('ckb').select(choice)
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name] return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
}, },
} satisfies LocaleObjectData), } satisfies LocaleObjectData,
({ {
code: 'fa-IR', code: 'fa-IR',
file: 'fa-IR.json', file: 'fa-IR.json',
name: 'فارسی', name: 'فارسی',
@ -110,9 +108,8 @@ const locales: LocaleObjectData[] = [
const name = new Intl.PluralRules('fa-IR').select(choice) const name = new Intl.PluralRules('fa-IR').select(choice)
return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name] return { zero: 0, one: 1, two: 2, few: 3, many: 4, other: 5 }[name]
}, },
} satisfies LocaleObjectData), } satisfies LocaleObjectData,
{ {
// @ts-expect-error ca used as placeholder
code: 'ca', code: 'ca',
file: 'ca.json', file: 'ca.json',
name: 'Català', name: 'Català',
@ -153,7 +150,6 @@ const locales: LocaleObjectData[] = [
name: 'Nederlands', name: 'Nederlands',
}, },
{ {
// @ts-expect-error es used as placeholder
code: 'es', code: 'es',
file: 'es.json', file: 'es.json',
name: 'Español', name: 'Español',
@ -207,7 +203,6 @@ const locales: LocaleObjectData[] = [
}, },
}, },
{ {
// @ts-expect-error pt used as placeholder
code: 'pt', code: 'pt',
file: 'pt.json', file: 'pt.json',
name: 'Português', name: 'Português',
@ -242,6 +237,11 @@ const locales: LocaleObjectData[] = [
file: 'it-IT.json', file: 'it-IT.json',
name: 'Italiano', name: 'Italiano',
}, },
{
code: 'sv',
file: 'sv.json',
name: 'Svenska',
},
{ {
code: 'th-TH', code: 'th-TH',
file: 'th-TH.json', file: 'th-TH.json',
@ -283,7 +283,7 @@ function buildLocales() {
acc.push(data) acc.push(data)
} }
return acc return acc
}, <LocaleObjectData[]>[]) }, [] as LocaleObjectData[])
return useLocales.sort((a, b) => a.code.localeCompare(b.code)) return useLocales.sort((a, b) => a.code.localeCompare(b.code))
} }
@ -313,7 +313,7 @@ export const datetimeFormats = Object.values(currentLocales).reduce((acc, data)
} }
return acc return acc
}, <DateTimeFormats>{}) }, {} as DateTimeFormats)
export const numberFormats = Object.values(currentLocales).reduce((acc, data) => { export const numberFormats = Object.values(currentLocales).reduce((acc, data) => {
const numberFormats = data.numberFormats const numberFormats = data.numberFormats
@ -345,7 +345,7 @@ export const numberFormats = Object.values(currentLocales).reduce((acc, data) =>
} }
return acc return acc
}, <NumberFormats>{}) }, {} as NumberFormats)
export const pluralRules = Object.values(currentLocales).reduce((acc, data) => { export const pluralRules = Object.values(currentLocales).reduce((acc, data) => {
const pluralRule = data.pluralRule const pluralRule = data.pluralRule
@ -355,4 +355,4 @@ export const pluralRules = Object.values(currentLocales).reduce((acc, data) => {
} }
return acc return acc
}, <PluralizationRules>{}) }, {} as PluralizationRules)

View file

@ -6,5 +6,10 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
volumes:
# make sure this directory has the same ownership as the elk user from the Dockerfile
# otherwise Elk will not be able to store configs for accounts
# e.g., mkdir ./elk-storage; sudo chown 911:911 ./elk-storage
- './elk-storage:/elk/data'
ports: ports:
- 5314:5314 - 5314:5314

View file

@ -1,35 +1,41 @@
export default defineAppConfig({ export default defineAppConfig({
docus: { seo: {
title: 'Elk', title: 'Elk',
description: 'A nimble Mastodon web client.', description: 'A nimble Mastodon web client with modern features and elegant design.',
image: 'https://docs.elk.zone/elk-screenshot.png', },
socials: { header: {
// twitter: 'elk_zone', title: 'Elk',
github: 'elk-zone/elk', logo: {
mastodon: { alt: 'Elk',
label: 'Mastodon', light: '/logo.svg',
icon: 'IconMastodon', dark: '/logo.svg',
href: 'https://elk.zone/@elk@webtoo.ls',
},
}, },
aside: { },
level: 0, socials: {
exclude: [], github: 'https://github.com/elk-zone/elk',
}, mastodon: 'https://elk.zone/@elk@webtoo.ls',
header: { },
logo: true, github: {
showLinkIcon: true, url: 'https://github.com/elk-zone/elk',
exclude: [], branch: 'main',
}, rootDir: 'docs',
footer: { },
iconLinks: [ toc: {
title: 'On this page',
bottom: {
title: 'Community',
links: [
{ {
href: 'https://nuxt.com', icon: 'i-ph-shooting-star-duotone',
icon: 'IconNuxtLabs', label: 'Star on GitHub',
to: 'https://github.com/elk-zone/elk',
target: '_blank',
}, },
{ {
href: 'https://m.webtoo.ls/@elk', icon: 'i-simple-icons-mastodon',
icon: 'IconMastodon', label: 'Follow on Mastodon',
to: 'https://elk.zone/@elk@webtoo.ls',
target: '_blank',
}, },
], ],
}, },

View file

@ -1,5 +0,0 @@
<template>
<AppLayout>
<NuxtPage />
</AppLayout>
</template>

View file

@ -0,0 +1,4 @@
/* Elk brand colors for light and dark modes */
:root {
--ui-primary: #f0943c;
}

View file

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TranslationStatus } from '../../types' import type { TranslationStatus } from '../../../types'
const localesStatuses: TranslationStatus = await import('../../translation-status.json').then(m => m.default) const localesStatuses: TranslationStatus = await import('../../../translation-status.json').then(m => m.default)
const totalReference = localesStatuses.en.total const totalReference = localesStatuses.en!.total
type Tab = 'missing' | 'outdated' type Tab = 'missing' | 'outdated'
@ -32,7 +32,7 @@ const missingEntries = computed<string[]>(() => {
if (hidden.value || !currentLocale.value || localeTab.value !== 'missing') if (hidden.value || !currentLocale.value || localeTab.value !== 'missing')
return [] return []
return localesStatuses[locale.value].missing return localesStatuses[locale.value]!.missing
}) })
const outdatedEntries = computed<string[]>(() => { const outdatedEntries = computed<string[]>(() => {

View file

@ -1,36 +0,0 @@
---
title: Elk
navigation: false
layout: page
---
::block-hero
---
cta:
- Read more
- /guide
secondary:
- Try it out →
- https://elk.zone
---
#title
Elk
#description
An in-progress, nimble Mastodon web client
#support
![Screenshot of Elk](/screenshot.png)
#extra
::list
- markdown support
- code blocks
- reordering and connecting posts in timelines
- multi account
- GitHub HTML cards
- and more...
::
::

View file

@ -1,3 +1,8 @@
---
title: Introduction
description: Get started with Elk, the nimble Mastodon web client.
---
# Introduction # Introduction
## What is Elk? ## What is Elk?
@ -16,7 +21,7 @@ Elk provides some features not available through the standard Mastodon web app i
You can use Elk right in your browser. You can use Elk right in your browser.
On a mobile device, you can install the app to your home screen right from your browser for easy access. On a mobile device, you can install the app to your home screen right from your browser for easy access.
(This is called a Progressive Web App, or [PWA](../80.pwa.md).) (This is called a Progressive Web App, or [PWA](../pwa.md).)
Want to try it out? Want to try it out?
Visit https://elk.zone, type in your Mastodon server address, then log in. Visit https://elk.zone, type in your Mastodon server address, then log in.
@ -50,7 +55,7 @@ Using a client, you can
- View, add, or participate in polls - View, add, or participate in polls
- Follow, unfollow, mute, and block accounts - Follow, unfollow, mute, and block accounts
::alert{type="info"} ::callout{type="info"}
**Note:** Not all clients provide all features. **Note:** Not all clients provide all features.
:: ::

View file

@ -1,6 +1,11 @@
---
title: Features
description: Discover the features that make Elk a delightful Mastodon client.
---
# Features # Features
::alert{type=warning} ::callout{type=warning}
🚧 This section is a work in progress. 🚧 🚧 This section is a work in progress. 🚧
:: ::
@ -23,7 +28,7 @@ Elk renders basic Markdown-like text markup in post texts as the expected HTML.
- Use one asterisk (`*`) before and after a word or phrase to *italicize** the text. - Use one asterisk (`*`) before and after a word or phrase to *italicize** the text.
- Surround the text with two asterisks (`**word**`) to **bold** it. - Surround the text with two asterisks (`**word**`) to **bold** it.
::alert{type="warning"} ::callout{type="warning"}
Many apps do not support Markdown in posts. Many apps do not support Markdown in posts.
Mastodon itself does not support Markdown in posts. Mastodon itself does not support Markdown in posts.

View file

@ -1,3 +1,8 @@
---
title: Contributing
description: Learn how to contribute to Elk and help build the future of Mastodon clients.
---
# Contributing # Contributing
We're really excited that you're interested in contributing to Elk! Before submitting your contribution, please read through the following guide. We're really excited that you're interested in contributing to Elk! Before submitting your contribution, please read through the following guide.

View file

@ -1,3 +1,8 @@
---
title: Sponsoring
description: Support the development of Elk by sponsoring the team.
---
# Sponsoring # Sponsoring
If you're enjoying the app, consider sponsoring our team: If you're enjoying the app, consider sponsoring our team:

View file

@ -1,4 +1,7 @@
# Netlify and Cloudflare ---
title: Netlify and Cloudflare
description: Deploy your own Elk instance.
---
Want to host Elk for your Mastodon instance? You came to the right place! Want to host Elk for your Mastodon instance? You came to the right place!

View file

@ -1,3 +1,8 @@
---
title: Progressive Web App
description: Learn about Elk's PWA capabilities and how to install it on your device.
---
# PWA # PWA
Elk provides a PWA (Progressive Web App) that can be installed on your desktop/device. This allows you to use Elk as a native app on your device, and it will work offline. Elk provides a PWA (Progressive Web App) that can be installed on your desktop/device. This allows you to use Elk as a native app on your device, and it will work offline.

View file

@ -1,3 +1,8 @@
---
title: Privacy Policy
description: Elk's privacy policy and data handling practices.
---
# Privacy # Privacy
> Last updated January 27, 2023 > Last updated January 27, 2023
@ -8,7 +13,7 @@ This privacy notice for Elk describes how we handle your information when you:
- Download and use our mobile or desktop application (Elk) - Download and use our mobile or desktop application (Elk)
::alert{type=warning} ::callout{type="warning"}
Elk is [open source](https://github.com/elk-zone/elk) and other websites that link to this privacy notice may not be affiliated with Elk or bound by this policy. Elk is [open source](https://github.com/elk-zone/elk) and other websites that link to this privacy notice may not be affiliated with Elk or bound by this policy.
:: ::

158
docs/content/index.md Executable file
View file

@ -0,0 +1,158 @@
---
seo:
title: "Elk - A Nimble Mastodon Web Client"
description: A nimble Mastodon web client that provides a fresh and intuitive social media experience with modern features and elegant design.
ogImage: https://docs.elk.zone/elk-screenshot.png
---
::u-page-hero
---
orientation: horizontal
---
:::div{.hidden.lg:flex.items-center.justify-center}
![Elk Screenshot](/screenshot.png){.rounded-lg.shadow-lg}
:::
#title
A Nimble [Mastodon Web Client]{.text-primary}
#description
Experience Mastodon like never before. Elk brings you a fresh, intuitive interface with modern features that make social networking delightful.
#links
:::u-button
---
icon: i-ph-rocket-launch-duotone
size: xl
to: /guide
---
Read the docs
:::
:::u-button
---
icon: i-ph-arrow-square-out-duotone
size: xl
variant: outline
to: https://elk.zone
target: _blank
---
Try it live
:::
::
::u-page-section
#title
Everything you need for the perfect [Mastodon experience]{.text-primary}
#features
:::u-page-card
---
spotlight: true
icon: i-ph-markdown-logo-duotone
to: /guide/features
---
#title
Rich Content Support
#description
Full Markdown support with syntax highlighting, emoji reactions, and rich media previews that bring your posts to life.
:::
:::u-page-card
---
spotlight: true
icon: i-ic-twotone-view-timeline
to: /guide/features
---
#title
Smart Timeline Management
#description
Reorder and connect posts in your timeline with intelligent grouping and enhanced notification management.
:::
:::u-page-card
---
spotlight: true
icon: i-ph-users-duotone
to: /guide/features
---
#title
Multi-Account Support
#description
Seamlessly manage multiple Mastodon accounts with quick switching and unified notifications.
:::
:::u-page-card
---
spotlight: true
icon: i-ph-github-logo-duotone
to: /guide/features
---
#title
GitHub Integration
#description
Beautiful HTML cards for GitHub links with repository previews and rich metadata display.
:::
:::u-page-card
---
spotlight: true
icon: i-ph-device-mobile-duotone
to: /pwa
---
#title
Progressive Web App
#description
Install Elk on any device for a native-like experience with offline support and push notifications.
:::
:::u-page-card
---
spotlight: true
icon: i-ph-heart-duotone
to: /guide/contributing
---
#title
Open Source & Community Driven
#description
Built with love by the community. Contribute to the future of federated social media.
:::
::
::u-page-section
---
orientation: horizontal
---
#title
Ready to dive in?
#description
Join thousands of users who have already discovered a better way to experience Mastodon. Get started in minutes with our comprehensive documentation.
#links
:::u-button
---
icon: i-ph-book-open-duotone
size: xl
to: /guide
---
Read the docs
:::
:::u-button
---
icon: i-ph-github-logo-duotone
size: xl
variant: outline
to: https://github.com/elk-zone/elk
target: _blank
---
View on GitHub
:::
::

View file

@ -1,5 +1,17 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
extends: '@nuxt-themes/docus', extends: ['docus'],
css: ['~/assets/css/main.css'],
site: {
name: 'Elk',
url: 'https://docs.elk.zone',
},
llms: {
domain: 'https://docs.elk.zone',
},
vite: { vite: {
optimizeDeps: { optimizeDeps: {

View file

@ -3,16 +3,14 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "nuxi dev", "dev": "nuxt dev",
"build": "nuxi build", "build": "nuxt build",
"generate": "nuxi generate", "generate": "nuxt generate",
"preview": "nuxi preview" "preview": "nuxt preview"
}, },
"dependencies": { "dependencies": {
"theme-colors": "^0.1.0" "better-sqlite3": "^12.4.1",
}, "docus": "^4.0.0",
"devDependencies": { "nuxt": "^4.1.2"
"@nuxt-themes/docus": "^1.15.1",
"nuxt": "^3.18.1"
} }
} }

View file

@ -1,17 +0,0 @@
import { defineTheme } from 'pinceau'
import { getColors } from 'theme-colors'
const light = getColors('#995e1b')
const primary = Object
.entries(getColors('#d98018'))
.reduce((acc, [key, value]) => {
acc[key] = {
initial: light[key]!,
dark: value,
}
return acc
}, {} as Record<string | number, { initial: string, dark: string }>)
export default defineTheme({
color: { primary },
})

View file

@ -7,6 +7,8 @@
"route_loaded": "Page {0} chargée" "route_loaded": "Page {0} chargée"
}, },
"account": { "account": {
"authorize": "Autoriser l'abonnement",
"authorized": "Vous avez autorisé la demande",
"avatar_description": "Avatar de {0}", "avatar_description": "Avatar de {0}",
"blocked_by": "Ce compte vous a bloqué", "blocked_by": "Ce compte vous a bloqué",
"blocked_domains": "Domaines bloqués", "blocked_domains": "Domaines bloqués",
@ -25,6 +27,7 @@
"follows_you": "@:account.follow_back", "follows_you": "@:account.follow_back",
"go_to_profile": "Aller à son profil", "go_to_profile": "Aller à son profil",
"joined": "a rejoint", "joined": "a rejoint",
"lock": "Verrouiller",
"moved_title": "a indiqué que son nouveau compte est désormais :", "moved_title": "a indiqué que son nouveau compte est désormais :",
"muted_users": "Comptes masqués", "muted_users": "Comptes masqués",
"muting": "Masqué·e", "muting": "Masqué·e",
@ -37,7 +40,10 @@
"profile_description": "En-tête du profil de {0}", "profile_description": "En-tête du profil de {0}",
"profile_personal_note": "Note personnelle", "profile_personal_note": "Note personnelle",
"profile_unavailable": "Profil non accessible", "profile_unavailable": "Profil non accessible",
"reject": "Rejeter l'abonnement",
"rejected": "Vous avez rejeté la demande",
"request_follow": "Demander à suivre", "request_follow": "Demander à suivre",
"requested": "{0} a demandé à vous suivre",
"unblock": "Débloquer", "unblock": "Débloquer",
"unfollow": "Ne plus suivre", "unfollow": "Ne plus suivre",
"unmute": "Réafficher", "unmute": "Réafficher",
@ -52,6 +58,7 @@
"boost": "Partager", "boost": "Partager",
"boost_count": "{0}", "boost_count": "{0}",
"boosted": "Partagé", "boosted": "Partagé",
"clear": "Effacer",
"clear_publish_failed": "Effacer les erreurs de publication", "clear_publish_failed": "Effacer les erreurs de publication",
"clear_save_failed": "Effacer les erreurs de sauvegarde", "clear_save_failed": "Effacer les erreurs de sauvegarde",
"clear_upload_failed": "Effacer les erreurs de téléversement de fichier", "clear_upload_failed": "Effacer les erreurs de téléversement de fichier",
@ -66,8 +73,10 @@
"favourited": "J'aime", "favourited": "J'aime",
"more": "Plus", "more": "Plus",
"next": "Suivant", "next": "Suivant",
"open_image_preview_dialog": "Ouvrir le dialogue d'aperçu de l'image",
"prev": "Précédent", "prev": "Précédent",
"publish": "Publier", "publish": "Publier",
"publish_thread": "Publier le fil",
"reply": "Répondre", "reply": "Répondre",
"reply_count": "{0}", "reply_count": "{0}",
"reset": "Réinitialiser", "reset": "Réinitialiser",
@ -115,12 +124,14 @@
"block_account": { "block_account": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Bloquer", "confirm": "Bloquer",
"description": "Voulez-vous vraiment bloquer {0} ?" "description": "Voulez-vous vraiment bloquer {0} ?",
"title": "Bloquer le compte"
}, },
"block_domain": { "block_domain": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Bloquer", "confirm": "Bloquer",
"description": "Voulez-vous vraiment bloquer {0} ?" "description": "Voulez-vous vraiment bloquer {0} ?",
"title": "Bloquer le domaine"
}, },
"common": { "common": {
"cancel": "Non", "cancel": "Non",
@ -129,27 +140,37 @@
"delete_list": { "delete_list": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Supprimer", "confirm": "Supprimer",
"description": "Voulez-vous vraiment supprimer la liste \"{0}\" ?" "description": "Voulez-vous vraiment supprimer la liste \"{0}\" ?",
"title": "Supprimer la liste"
}, },
"delete_posts": { "delete_posts": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Supprimer", "confirm": "Supprimer",
"description": "Voulez-vous vraiment supprimer ce message ?" "description": "Voulez-vous vraiment supprimer ce message ?",
"title": "Supprimer le message"
}, },
"mute_account": { "mute_account": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Mettre en sourdine", "confirm": "Mettre en sourdine",
"description": "Voulez-vous vraiment mettre en sourdine {0} ?" "days": "jour|jour|jour",
"description": "Voulez-vous vraiment mettre en sourdine {0} ?",
"hours": "heures|heures|heures",
"minute": "minutes|minutes|minutes",
"notifications": "Mettre en sourdine les notifications",
"specify_duration": "Spécifier la durée de la mise en sourdine",
"title": "Mettre en sourdine le compte"
}, },
"show_reblogs": { "show_reblogs": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Afficher", "confirm": "Afficher",
"description": "Voulez-vous vraiment afficher les partages de {0} ?" "description": "Voulez-vous vraiment afficher les partages de {0} ?",
"title": "Afficher les partages"
}, },
"unfollow": { "unfollow": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Se désabonner", "confirm": "Se désabonner",
"description": "Voulez-vous vraiment vous désabonner ?" "description": "Voulez-vous vraiment vous désabonner ?",
"title": "Se désabonner"
} }
}, },
"conversation": { "conversation": {
@ -202,9 +223,12 @@
"error": "Il y a eu une erreur lors de la création de la liste", "error": "Il y a eu une erreur lors de la création de la liste",
"error_prefix": "Erreur :", "error_prefix": "Erreur :",
"list_title_placeholder": "Nom de la liste", "list_title_placeholder": "Nom de la liste",
"manage": "Gérer les listes",
"modify_account": "Modifier les listes de ce compte", "modify_account": "Modifier les listes de ce compte",
"remove_account": "Supprimer ce compte de listes", "remove_account": "Supprimer ce compte de listes",
"save": "Enregistrer les changements" "save": "Enregistrer les changements",
"search_following_desc": "Chercher des personnes que vous suivez",
"search_following_placeholder": "Chercher parmi les personnes que vous suivez"
}, },
"magic_keys": { "magic_keys": {
"dialog_header": "Raccourcis clavier", "dialog_header": "Raccourcis clavier",
@ -214,14 +238,26 @@
"command_mode": "Mode commande", "command_mode": "Mode commande",
"compose": "Composer", "compose": "Composer",
"favourite": "J'aime", "favourite": "J'aime",
"search": "Rechercher",
"show_new_items": "Afficher les nouveaux éléments",
"title": "Actions" "title": "Actions"
}, },
"media": { "media": {
"title": "Média" "title": "Média"
}, },
"navigation": { "navigation": {
"go_to_bookmarks": "Signets",
"go_to_conversations": "Conversations",
"go_to_explore": "Explorer",
"go_to_favourites": "Favoris",
"go_to_federated": "Fédérés",
"go_to_home": "Accueil", "go_to_home": "Accueil",
"go_to_lists": "Listes",
"go_to_local": "Local",
"go_to_notifications": "Notifications", "go_to_notifications": "Notifications",
"go_to_profile": "Profil",
"go_to_search": "Rechercher",
"go_to_settings": "Paramètres",
"next_status": "Message suivant", "next_status": "Message suivant",
"previous_status": "Message précédent", "previous_status": "Message précédent",
"shortcut_help": "Aide sur les raccourcis", "shortcut_help": "Aide sur les raccourcis",
@ -276,13 +312,16 @@
"built_at": "Dernière compilation {0}", "built_at": "Dernière compilation {0}",
"compose": "Composer", "compose": "Composer",
"conversations": "Conversations", "conversations": "Conversations",
"docs": "Documentation",
"explore": "Explorer", "explore": "Explorer",
"favourites": "Aimés", "favourites": "Aimés",
"federated": "Fédérés", "federated": "Fédérés",
"hashtags": "Hashtags",
"home": "Accueil", "home": "Accueil",
"list": "Liste", "list": "Liste",
"lists": "Listes", "lists": "Listes",
"local": "Local", "local": "Local",
"more_menu": "Plus d'options",
"muted_users": "Comptes masqués", "muted_users": "Comptes masqués",
"notifications": "Notifications", "notifications": "Notifications",
"privacy": "Données privées", "privacy": "Données privées",
@ -297,10 +336,12 @@
"zen_mode": "Mode Zen" "zen_mode": "Mode Zen"
}, },
"notification": { "notification": {
"and": "et",
"favourited_post": "a aimé votre message", "favourited_post": "a aimé votre message",
"followed_you": "vous suit", "followed_you": "vous suit",
"followed_you_count": "{0} personnes vous suivent|{0} personne vous suit|{0} personnes vous suivent", "followed_you_count": "{0} personnes vous suivent|{0} personne vous suit|{0} personnes vous suivent",
"missing_type": "MISSING notification.type:", "missing_type": "MISSING notification.type:",
"others": "{0} personnes|{0} personne|{0} personnes",
"reblogged_post": "a relayé votre message", "reblogged_post": "a relayé votre message",
"reported": "{0} a signalé {1}", "reported": "{0} a signalé {1}",
"request_to_follow": "vous demande de le suivre", "request_to_follow": "vous demande de le suivre",
@ -417,6 +458,8 @@
"label": "Paramètres de compte" "label": "Paramètres de compte"
}, },
"interface": { "interface": {
"bottom_nav": "Navigation inférieure",
"bottom_nav_instructions": "Choisissez jusqu'à cinq boutons de navigation inférieure favoris. Doit inclure le bouton \"Plus d'options\".",
"color_mode": "Couleur de thème", "color_mode": "Couleur de thème",
"dark_mode": "Mode sombre", "dark_mode": "Mode sombre",
"default": " (par défaut)", "default": " (par défaut)",
@ -428,6 +471,7 @@
}, },
"language": { "language": {
"display_language": "Langue d'affichage", "display_language": "Langue d'affichage",
"how_to_contribute": "Comment contribuer ?",
"label": "Langue", "label": "Langue",
"post_language": "Langue de publication", "post_language": "Langue de publication",
"status": "État de la traduction : {0}/{1} ({2} %)", "status": "État de la traduction : {0}/{1} ({2} %)",
@ -495,6 +539,8 @@
}, },
"notifications_settings": "Notifications", "notifications_settings": "Notifications",
"preferences": { "preferences": {
"embedded_media": "Lecteur multimédia intégré",
"embedded_media_description": "Affichez un lecteur intégré au lieu de la carte d'aperçu normale lors de l'expansion des liens de streaming de supports partagés.",
"enable_autoplay": "Activer la lecture automatique", "enable_autoplay": "Activer la lecture automatique",
"enable_data_saving": "Activer l'économie de données", "enable_data_saving": "Activer l'économie de données",
"enable_data_saving_description": "Economise les données en évitant le chargement automatique des médias.", "enable_data_saving_description": "Economise les données en évitant le chargement automatique des médias.",
@ -507,13 +553,16 @@
"hide_boost_count": "Masquer les compteurs de partages", "hide_boost_count": "Masquer les compteurs de partages",
"hide_favorite_count": "Masquer les compteurs de favoris", "hide_favorite_count": "Masquer les compteurs de favoris",
"hide_follower_count": "Masquer les compteurs d'abonné·e·s", "hide_follower_count": "Masquer les compteurs d'abonné·e·s",
"hide_gif_indi_on_posts": "Masquer l'indicateur de gif sur les messages",
"hide_news": "Masquer les actualités", "hide_news": "Masquer les actualités",
"hide_reply_count": "Masquer les compteurs de réponses", "hide_reply_count": "Masquer les compteurs de réponses",
"hide_translation": "Masquer traduction", "hide_translation": "Masquer traduction",
"hide_username_emojis": "Masquer les emojis sur le nom d'utilisateur", "hide_username_emojis": "Masquer les emojis sur le nom d'utilisateur",
"hide_username_emojis_description": "Masque les emojis des noms d'utilisateur dans la timeline. \nLes emojis seront toujours visibles sur leurs profils.", "hide_username_emojis_description": "Masque les emojis des noms d'utilisateur dans la timeline. \nLes emojis seront toujours visibles sur leurs profils.",
"label": "Préférences", "label": "Préférences",
"optimize_for_low_performance_device": "Optimiser pour un dispositif à faible performance",
"title": "Fonctionnalités expérimentales", "title": "Fonctionnalités expérimentales",
"unmute_videos": "Son de vidéo par défaut",
"use_star_favorite_icon": "Utiliser l'icône de l'étoile en favoris", "use_star_favorite_icon": "Utiliser l'icône de l'étoile en favoris",
"user_picker": "User Picker", "user_picker": "User Picker",
"user_picker_description": "Affiche tous les avatars des comptes connectés en bas à gauche afin que vous puissiez basculer rapidement entre eux.", "user_picker_description": "Affiche tous les avatars des comptes connectés en bas à gauche afin que vous puissiez basculer rapidement entre eux.",
@ -556,7 +605,11 @@
}, },
"state": { "state": {
"attachments_exceed_server_limit": "Le nombre de pièces jointes a dépassé la limite par message.", "attachments_exceed_server_limit": "Le nombre de pièces jointes a dépassé la limite par message.",
"attachments_limit_audio_error": "Taille maximum d'audio dépassée : {0}",
"attachments_limit_error": "Limite par publication dépassée", "attachments_limit_error": "Limite par publication dépassée",
"attachments_limit_image_error": "Taille maximum d'image dépassée : {0}",
"attachments_limit_unknown_error": "Taille maximum de fichier dépassée : {0}",
"attachments_limit_video_error": "Taille maximum de vidéo dépassée : {0}",
"edited": "(Édité)", "edited": "(Édité)",
"editing": "Édition", "editing": "Édition",
"loading": "Chargement...", "loading": "Chargement...",
@ -573,15 +626,18 @@
}, },
"boosted_by": "Partagé par", "boosted_by": "Partagé par",
"edited": "Edité {0}", "edited": "Edité {0}",
"embedded_warning": "Lire ceci peut révéler votre adresse IP à d'autres.",
"favourited_by": "Aimé par", "favourited_by": "Aimé par",
"filter_hidden_phrase": "Filtré par", "filter_hidden_phrase": "Filtré par",
"filter_show_anyway": "Montrer coûte que coûte", "filter_show_anyway": "Montrer coûte que coûte",
"gif": "GIF",
"img_alt": { "img_alt": {
"ALT": "ALT", "ALT": "ALT",
"desc": "Description", "desc": "Description",
"dismiss": "Fermer", "dismiss": "Fermer",
"read": "Lire la description de {0}" "read": "Lire la description de {0}"
}, },
"pinned": "Messages épinglés",
"poll": { "poll": {
"count": "{0} votes", "count": "{0} votes",
"ends": "se clôt {0}", "ends": "se clôt {0}",
@ -663,6 +719,7 @@
"year_past": "il y a 0 année|l'année dernière|il y a {n} années" "year_past": "il y a 0 année|l'année dernière|il y a {n} années"
}, },
"timeline": { "timeline": {
"no_posts": "Pas de messages ici !",
"show_new_items": "Voir le nouveau message|Voir les {v} nouveaux messages", "show_new_items": "Voir le nouveau message|Voir les {v} nouveaux messages",
"view_older_posts": "Les messages plus anciens d'autres instances peuvent ne pas être affichés." "view_older_posts": "Les messages plus anciens d'autres instances peuvent ne pas être affichés."
}, },
@ -675,6 +732,7 @@
"add_emojis": "Ajouter des émoticônes", "add_emojis": "Ajouter des émoticônes",
"add_media": "Ajouter des images, une vidéo ou un fichier audio", "add_media": "Ajouter des images, une vidéo ou un fichier audio",
"add_publishable_content": "Ajouter du contenu à publier", "add_publishable_content": "Ajouter du contenu à publier",
"add_thread_item": "Ajouter un message au fil",
"change_content_visibility": "Ajuster la confidentialité du message", "change_content_visibility": "Ajuster la confidentialité du message",
"change_language": "Changer la langue", "change_language": "Changer la langue",
"emoji": "Emoji", "emoji": "Emoji",
@ -684,6 +742,8 @@
"open_editor_tools": "Outils d'édition", "open_editor_tools": "Outils d'édition",
"pick_an_icon": "Choisir une icône", "pick_an_icon": "Choisir une icône",
"publish_failed": "Fermez les messages ayant échoué en haut de l'éditeur pour republier les messages", "publish_failed": "Fermez les messages ayant échoué en haut de l'éditeur pour republier les messages",
"remove_thread_item": "Supprimer le message du fil",
"start_thread": "Commencer un fil",
"toggle_bold": "Appliquer/retirer le gras", "toggle_bold": "Appliquer/retirer le gras",
"toggle_code_block": "Ajouter un bloc de code", "toggle_code_block": "Ajouter un bloc de code",
"toggle_italic": "Appliquer/retirer l'italique" "toggle_italic": "Appliquer/retirer l'italique"

View file

@ -27,6 +27,7 @@
"follows_you": "Mengikutimu", "follows_you": "Mengikutimu",
"go_to_profile": "Buka profil", "go_to_profile": "Buka profil",
"joined": "Bergabung", "joined": "Bergabung",
"lock": "Kunci",
"moved_title": "telah mengindikasikan bahwa akun baru mereka sekarang:", "moved_title": "telah mengindikasikan bahwa akun baru mereka sekarang:",
"muted_users": "Pengguna dibisukan", "muted_users": "Pengguna dibisukan",
"muting": "Bisukan", "muting": "Bisukan",
@ -57,6 +58,7 @@
"boost": "Dukung", "boost": "Dukung",
"boost_count": "{0}", "boost_count": "{0}",
"boosted": "Didukung", "boosted": "Didukung",
"clear": "Bersihkan",
"clear_publish_failed": "Hapus error publikasi", "clear_publish_failed": "Hapus error publikasi",
"clear_save_failed": "Hapus kesalahan penyimpanan", "clear_save_failed": "Hapus kesalahan penyimpanan",
"clear_upload_failed": "Hapus kesalahan pengunggahan file", "clear_upload_failed": "Hapus kesalahan pengunggahan file",
@ -71,8 +73,10 @@
"favourited": "Difavoritkan", "favourited": "Difavoritkan",
"more": "Selengkapnya", "more": "Selengkapnya",
"next": "Selanjutnya", "next": "Selanjutnya",
"open_image_preview_dialog": "Buka dialog pratinjau gambar",
"prev": "Sebelumnya", "prev": "Sebelumnya",
"publish": "Terbitkan", "publish": "Terbitkan",
"publish_thread": "Terbitkan thread",
"reply": "Balas", "reply": "Balas",
"reply_count": "{0}", "reply_count": "{0}",
"reset": "Atur ulang", "reset": "Atur ulang",
@ -120,12 +124,14 @@
"block_account": { "block_account": {
"cancel": "Batalkan", "cancel": "Batalkan",
"confirm": "Blokir", "confirm": "Blokir",
"description": "Yakin ingin memblokir {0}?" "description": "Yakin ingin memblokir {0}?",
"title": "Blokir akun"
}, },
"block_domain": { "block_domain": {
"cancel": "Batalkan", "cancel": "Batalkan",
"confirm": "Blokir", "confirm": "Blokir",
"description": "Yakin ingin memblokir {0}?" "description": "Yakin ingin memblokir {0}?",
"title": "Blokir domain"
}, },
"common": { "common": {
"cancel": "Tidak", "cancel": "Tidak",
@ -134,27 +140,37 @@
"delete_list": { "delete_list": {
"cancel": "Batalkan", "cancel": "Batalkan",
"confirm": "Hapus", "confirm": "Hapus",
"description": "Apakah Anda yakin ingin menghapus daftar \"{0}\"?" "description": "Apakah Anda yakin ingin menghapus daftar \"{0}\"?",
"title": "Hapus daftar"
}, },
"delete_posts": { "delete_posts": {
"cancel": "Batalkan", "cancel": "Batalkan",
"confirm": "Hapus", "confirm": "Hapus",
"description": "Yakin ingin menghapus postingan ini?" "description": "Yakin ingin menghapus postingan ini?",
"title": "Hapus postingan"
}, },
"mute_account": { "mute_account": {
"cancel": "Batalkan", "cancel": "Batalkan",
"confirm": "Bisukan", "confirm": "Bisukan",
"description": "Yakin ingin membisukan {0}?" "days": "hari|hari|hari",
"description": "Yakin ingin membisukan {0}?",
"hours": "jam|jam|jam",
"minute": "menit|menit|menit",
"notifications": "Bisukan notifikasi",
"specify_duration": "Tentukan durasi bisu",
"title": "Bisukan akun"
}, },
"show_reblogs": { "show_reblogs": {
"cancel": "Batalkan", "cancel": "Batalkan",
"confirm": "Tampilkan", "confirm": "Tampilkan",
"description": "Yakin ingin menampilkan dukungan dari {0}?" "description": "Yakin ingin menampilkan dukungan dari {0}?",
"title": "Tampilkan dukungan"
}, },
"unfollow": { "unfollow": {
"cancel": "Batalkan", "cancel": "Batalkan",
"confirm": "Berhenti mengikuti", "confirm": "Berhenti mengikuti",
"description": "Anda yakin ingin berhenti mengikuti?" "description": "Anda yakin ingin berhenti mengikuti?",
"title": "Berhenti mengikuti"
} }
}, },
"conversation": { "conversation": {
@ -207,9 +223,12 @@
"error": "Terjadi kesalahan saat membuat daftar", "error": "Terjadi kesalahan saat membuat daftar",
"error_prefix": "Kesalahan: ", "error_prefix": "Kesalahan: ",
"list_title_placeholder": "Judul daftar", "list_title_placeholder": "Judul daftar",
"manage": "Kelola daftar",
"modify_account": "Ubah daftar dengan akun", "modify_account": "Ubah daftar dengan akun",
"remove_account": "Hapus akun dari daftar", "remove_account": "Hapus akun dari daftar",
"save": "Simpan perubahan" "save": "Simpan perubahan",
"search_following_desc": "Cari orang yang Anda ikuti",
"search_following_placeholder": "Cari di antara orang yang Anda ikuti"
}, },
"magic_keys": { "magic_keys": {
"dialog_header": "Pintasan keyboard", "dialog_header": "Pintasan keyboard",
@ -220,14 +239,25 @@
"compose": "Menyusun", "compose": "Menyusun",
"favourite": "Favorit", "favourite": "Favorit",
"search": "Cari", "search": "Cari",
"show_new_items": "Tampilkan item baru",
"title": "Tindakan" "title": "Tindakan"
}, },
"media": { "media": {
"title": "Media" "title": "Media"
}, },
"navigation": { "navigation": {
"go_to_bookmarks": "Markah",
"go_to_conversations": "Percakapan",
"go_to_explore": "Jelajahi",
"go_to_favourites": "Favorit",
"go_to_federated": "Federasi",
"go_to_home": "Beranda", "go_to_home": "Beranda",
"go_to_lists": "Daftar",
"go_to_local": "Lokal",
"go_to_notifications": "Notifikasi", "go_to_notifications": "Notifikasi",
"go_to_profile": "Profil",
"go_to_search": "Cari",
"go_to_settings": "Pengaturan",
"next_status": "Posting selanjutnya", "next_status": "Posting selanjutnya",
"previous_status": "Posting sebelumnya", "previous_status": "Posting sebelumnya",
"shortcut_help": "Bantuan jalan pintas", "shortcut_help": "Bantuan jalan pintas",
@ -282,13 +312,16 @@
"built_at": "Diperbarui {0}", "built_at": "Diperbarui {0}",
"compose": "Menyusun", "compose": "Menyusun",
"conversations": "Percakapan", "conversations": "Percakapan",
"docs": "Dokumentasi",
"explore": "Jelajahi", "explore": "Jelajahi",
"favourites": "Favorit", "favourites": "Favorit",
"federated": "Federasi", "federated": "Federasi",
"hashtags": "Tagar",
"home": "Beranda", "home": "Beranda",
"list": "Daftar", "list": "Daftar",
"lists": "Daftar", "lists": "Daftar",
"local": "Lokal", "local": "Lokal",
"more_menu": "Menu lainnya",
"muted_users": "Pengguna dibisukan", "muted_users": "Pengguna dibisukan",
"notifications": "Notifikasi", "notifications": "Notifikasi",
"privacy": "Privasi", "privacy": "Privasi",
@ -303,10 +336,12 @@
"zen_mode": "Mode Zen" "zen_mode": "Mode Zen"
}, },
"notification": { "notification": {
"and": "dan",
"favourited_post": "memfavoritkan pos Anda", "favourited_post": "memfavoritkan pos Anda",
"followed_you": "mengikutimu", "followed_you": "mengikutimu",
"followed_you_count": "{0} orang mengikuti Anda|{0} orang mengikuti Anda|{0} orang mengikuti Anda", "followed_you_count": "{0} orang mengikuti Anda|{0} orang mengikuti Anda|{0} orang mengikuti Anda",
"missing_type": "NOTIFIKASI HILANG.jenis:", "missing_type": "NOTIFIKASI HILANG.jenis:",
"others": "{0} lainnya|{0} lainnya|{0} lainnya",
"reblogged_post": "meningkatkan postingan Anda", "reblogged_post": "meningkatkan postingan Anda",
"reported": "{0} melaporkan {1}", "reported": "{0} melaporkan {1}",
"request_to_follow": "meminta untuk mengikuti Anda", "request_to_follow": "meminta untuk mengikuti Anda",
@ -423,6 +458,8 @@
"label": "Pengaturan akun" "label": "Pengaturan akun"
}, },
"interface": { "interface": {
"bottom_nav": "Navigasi Bawah",
"bottom_nav_instructions": "Pilih tombol navigasi favorit Anda hingga lima untuk navigasi bawah. Harus menyertakan tombol \"Menu Lainnya\".",
"color_mode": "Tema", "color_mode": "Tema",
"dark_mode": "Gelap", "dark_mode": "Gelap",
"default": "(bawaan)", "default": "(bawaan)",
@ -434,6 +471,7 @@
}, },
"language": { "language": {
"display_language": "Bahasa tampilan", "display_language": "Bahasa tampilan",
"how_to_contribute": "Bagaimana cara berkontribusi?",
"label": "Bahasa", "label": "Bahasa",
"post_language": "Bahasa Postingan", "post_language": "Bahasa Postingan",
"status": "Status terjemahan: {0}/{1} ({2}%)", "status": "Status terjemahan: {0}/{1} ({2}%)",
@ -515,13 +553,17 @@
"hide_boost_count": "Sembunyikan jumlah dukungan", "hide_boost_count": "Sembunyikan jumlah dukungan",
"hide_favorite_count": "Sembunyikan jumlah favorit", "hide_favorite_count": "Sembunyikan jumlah favorit",
"hide_follower_count": "Sembunyikan jumlah pengikut", "hide_follower_count": "Sembunyikan jumlah pengikut",
"hide_gif_indi_on_posts": "Sembunyikan indikator gif pada postingan",
"hide_news": "Sembunyikan berita", "hide_news": "Sembunyikan berita",
"hide_reply_count": "Sembunyikan jumlah balasan", "hide_reply_count": "Sembunyikan jumlah balasan",
"hide_tag_hover_card": "Sembunyikan tag kartu hover",
"hide_translation": "Sembunyikan terjemahan", "hide_translation": "Sembunyikan terjemahan",
"hide_username_emojis": "Sembunyikan emoji di nama pengguna", "hide_username_emojis": "Sembunyikan emoji di nama pengguna",
"hide_username_emojis_description": "Sembunyikan emoji dari nama pengguna dalam timeline. Emoji tetap akan terlihat di profil mereka.", "hide_username_emojis_description": "Sembunyikan emoji dari nama pengguna dalam timeline. Emoji tetap akan terlihat di profil mereka.",
"label": "Preferensi", "label": "Preferensi",
"optimize_for_low_performance_device": "Optimalkan untuk perangkat berkinerja rendah",
"title": "Fitur Eksperimental", "title": "Fitur Eksperimental",
"unmute_videos": "Suara video aktif secara default",
"use_star_favorite_icon": "Gunakan ikon bintang favorit", "use_star_favorite_icon": "Gunakan ikon bintang favorit",
"user_picker": "Pemilih Pengguna", "user_picker": "Pemilih Pengguna",
"user_picker_description": "Menampilkan semua avatar akun yang dicatat di kiri bawah sehingga Anda dapat beralih antar akun dengan cepat.", "user_picker_description": "Menampilkan semua avatar akun yang dicatat di kiri bawah sehingga Anda dapat beralih antar akun dengan cepat.",
@ -564,7 +606,11 @@
}, },
"state": { "state": {
"attachments_exceed_server_limit": "Jumlah lampiran melebihi batas per posting.", "attachments_exceed_server_limit": "Jumlah lampiran melebihi batas per posting.",
"attachments_limit_audio_error": "Ukuran maksimal audio terlampaui: {0}",
"attachments_limit_error": "Batas per posting terlampaui", "attachments_limit_error": "Batas per posting terlampaui",
"attachments_limit_image_error": "Ukuran maksimal gambar terlampaui: {0}",
"attachments_limit_unknown_error": "Ukuran file maksimal terlampaui: {0}",
"attachments_limit_video_error": "Ukuran maksimal video terlampaui: {0}",
"edited": "(Disunting)", "edited": "(Disunting)",
"editing": "Menyunting", "editing": "Menyunting",
"loading": "Memuat...", "loading": "Memuat...",
@ -585,24 +631,27 @@
"favourited_by": "Difavoritkan Oleh", "favourited_by": "Difavoritkan Oleh",
"filter_hidden_phrase": "Difilter oleh", "filter_hidden_phrase": "Difilter oleh",
"filter_show_anyway": "Tetap tampilkan", "filter_show_anyway": "Tetap tampilkan",
"gif": "GIF",
"img_alt": { "img_alt": {
"ALT": "ALT", "ALT": "ALT",
"desc": "Keterangan", "desc": "Keterangan",
"dismiss": "Batalkan", "dismiss": "Batalkan",
"read": "Baca deskripsi {0}" "read": "Baca deskripsi {0}"
}, },
"pinned": "Postingan disematkan",
"poll": { "poll": {
"count": "{0} suara|{0} suara|{0} suara", "count": "{0} suara|{0} suara|{0} suara",
"ends": "berakhir {0}", "ends": "berakhir {0}",
"finished": "selesai {0}" "finished": "selesai {0}",
"update": "Perbarui jajak pendapat"
}, },
"replying_to": "Membalas ke {0}", "replying_to": "Membalas ke {0}",
"show_full_thread": "Tampilkan utas Penuh", "show_full_thread": "Tampilkan thread Penuh",
"someone": "seseorang", "someone": "seseorang",
"spoiler_media_hidden": "Media disembunyikan", "spoiler_media_hidden": "Media disembunyikan",
"spoiler_show_less": "Tampilkan lebih sedikit", "spoiler_show_less": "Tampilkan lebih sedikit",
"spoiler_show_more": "Menampilkan lebih banyak", "spoiler_show_more": "Menampilkan lebih banyak",
"thread": "Utas", "thread": "Thread",
"try_original_site": "Coba situs asli" "try_original_site": "Coba situs asli"
}, },
"status_history": { "status_history": {
@ -671,6 +720,7 @@
"year_past": "0 tahun lalu|tahun lalu|{n} tahun lalu" "year_past": "0 tahun lalu|tahun lalu|{n} tahun lalu"
}, },
"timeline": { "timeline": {
"no_posts": "Tidak ada postingan di sini!",
"show_new_items": "Tampilkan {v} item baru|Tampilkan {v} item baru|Tampilkan {v} item baru", "show_new_items": "Tampilkan {v} item baru|Tampilkan {v} item baru|Tampilkan {v} item baru",
"view_older_posts": "Posting lama dari contoh lain mungkin tidak ditampilkan." "view_older_posts": "Posting lama dari contoh lain mungkin tidak ditampilkan."
}, },
@ -683,6 +733,7 @@
"add_emojis": "Tambahkan emoji", "add_emojis": "Tambahkan emoji",
"add_media": "Tambahkan gambar, video atau file audio", "add_media": "Tambahkan gambar, video atau file audio",
"add_publishable_content": "Tambahkan konten untuk dipublikasikan", "add_publishable_content": "Tambahkan konten untuk dipublikasikan",
"add_thread_item": "Tambahkan item ke thread",
"change_content_visibility": "Ubah visibilitas konten", "change_content_visibility": "Ubah visibilitas konten",
"change_language": "Ubah bahasa", "change_language": "Ubah bahasa",
"emoji": "Emoji", "emoji": "Emoji",
@ -692,6 +743,8 @@
"open_editor_tools": "Alat Editor", "open_editor_tools": "Alat Editor",
"pick_an_icon": "Pilih ikon", "pick_an_icon": "Pilih ikon",
"publish_failed": "Tutup pesan yang gagal di bagian atas editor untuk mempublikasikan ulang postingan", "publish_failed": "Tutup pesan yang gagal di bagian atas editor untuk mempublikasikan ulang postingan",
"remove_thread_item": "Hapus item dari thread",
"start_thread": "Mulai thread",
"toggle_bold": "Ubah tebal", "toggle_bold": "Ubah tebal",
"toggle_code_block": "Tulis sebagai blok kode", "toggle_code_block": "Tulis sebagai blok kode",
"toggle_italic": "Alihkan miring" "toggle_italic": "Alihkan miring"

772
locales/sv.json Normal file
View file

@ -0,0 +1,772 @@
{
"a11y": {
"loading_page": "Laddar sidan, vänligen vänta",
"loading_titled_page": "Laddar sida {0}, vänligen vänta",
"locale_changed": "Språk ändrades till {0}",
"locale_changing": "Byter språk, vänligen vänta",
"route_loaded": "Sidan {0} laddades"
},
"account": {
"authorize": "Tillåt att följa",
"authorized": "Du har tillåtit förfrågan",
"avatar_description": "{0}'s profilbild",
"blocked_by": "Du är blockerad för denna användare.",
"blocked_domains": "Blockerade domäner",
"blocked_users": "Blockerade användare",
"blocking": "Blockerad",
"bot": "BOT",
"copy_account_name": "Kopiera kontonamn",
"favourites": "Favoriter",
"follow": "Följ",
"follow_back": "Följ tillbaka",
"follow_requested": "Begärd",
"followers": "Följare",
"followers_count": "{0} Följare|{0} Följare|{0} Följare",
"following": "Följer",
"following_count": "{0} Följer",
"follows_you": "Följer dig",
"go_to_profile": "Gå till profil",
"joined": "Gick med",
"lock": "Lås",
"moved_title": "har angett att deras konto är nu:",
"muted_users": "Tystade användare",
"muting": "Tystad",
"mutuals": "Ömsesidiga",
"notifications_on_post_disable": "Sluta notifiera mig när {username} gör inlägg",
"notifications_on_post_enable": "Notifiera mig när {username} gör inlägg",
"pinned": "Fäst",
"posts": "Inlägg",
"posts_count": "{0} Inlägg|{0} Inlägg|{0} Inlägg",
"profile_description": "{0}s profilrubrik",
"profile_personal_note": "Personlig anteckning",
"profile_unavailable": "Profil ej tillgänlig",
"reject": "Neka följare",
"rejected": "Du har nekat begäran",
"request_follow": "Begär att följa",
"requested": "{0} har begärt att följa dig",
"unblock": "Avblockera",
"unfollow": "Avfölj",
"unmute": "Avtysta",
"view_other_followers": "Följare från andra instanser kanske inte visas.",
"view_other_following": "Följande från andra instanser kanske inte visas.",
"withdraw_follow_request": "Återkalla följförfrågan"
},
"action": {
"apply": "Tillämpa",
"bookmark": "Bokmärk",
"bookmarked": "Bokmärkt",
"boost": "Boosta",
"boost_count": "{0}",
"boosted": "Boostad",
"clear": "Rensa",
"clear_publish_failed": "Rensa publiceringsfel",
"clear_save_failed": "Rensa sparfel",
"clear_upload_failed": "Rensa filuppladdningsfel",
"close": "Stäng",
"compose": "Komponera",
"confirm": "Bekräfta",
"done": "Klar",
"edit": "Redigera",
"enter_app": "Öppna appen",
"favourite": "Favoritmarkera",
"favourite_count": "{0}",
"favourited": "Favoritmarkerad",
"more": "Mer",
"next": "Nästa",
"open_image_preview_dialog": "Öppna bildförhandsgranskning",
"prev": "Föregående",
"publish": "Publicera",
"publish_thread": "Publicera tråd",
"reply": "Svara",
"reply_count": "{0}",
"reset": "Återställ",
"save": "Spara",
"save_changes": "Spara ändringar",
"sign_in": "Logga in",
"sign_in_to": "Logga in på {0}",
"switch_account": "Växla konto",
"vote": "Rösta"
},
"app_desc_short": "En smidig Mastodon-webbklient",
"app_logo": "Elk-logga",
"app_name": "Elk",
"attachment": {
"edit_title": "Beskrivning",
"remove_label": "Ta bort bilaga"
},
"command": {
"activate": "Aktivera",
"complete": "Färdigställ",
"compose_desc": "Skriv ett nytt inlägg",
"n_people_in_the_past_n_days": "{0} personer de senaste {1} dagarna",
"select_lang": "Välj språk",
"sign_in_desc": "Lägg till ett befintligt konto",
"switch_account": "Växla till {0}",
"switch_account_desc": "Växla till ett annat konto",
"toggle_dark_mode": "Växla mörkt läge",
"toggle_zen_mode": "Växla zen-läge"
},
"common": {
"end_of_list": "Slut på listan",
"error": "FEL",
"fetching": "Hämtar...",
"in": "i",
"no_bookmarks": "Inga bokmärkta inlägg än",
"no_favourites": "Inga favoritmarkerade inlägg än",
"not_found": "404 Ej hittad",
"offline_desc": "Verkar som att du är offline. Vänligen kontrollera din nätverksanslutning."
},
"compose": {
"draft_title": "Utkast {0}",
"drafts": "Utkast ({v})"
},
"confirm": {
"block_account": {
"cancel": "Avbryt",
"confirm": "Blockera",
"description": "Är du säker på att du vill blockera {0}?",
"title": "Blockera konto"
},
"block_domain": {
"cancel": "Avbryt",
"confirm": "Blockera",
"description": "Är du säker på att du vill blockera {0}?",
"title": "Blockera domän"
},
"common": {
"cancel": "Nej",
"confirm": "Ja"
},
"delete_list": {
"cancel": "Avbryt",
"confirm": "Radera",
"description": "Är du säker på att du vill radera \"{0}\"-listan?",
"title": "Radera lista"
},
"delete_posts": {
"cancel": "Avbryt",
"confirm": "Radera",
"description": "Är du säker på att du vill radera detta inlägg",
"title": "Radera inlägg"
},
"mute_account": {
"cancel": "Avbryt",
"confirm": "Tysta",
"days": "dagar|dag|dagar",
"description": "Är du säker på att du vill tysta {0}?",
"hours": "timmar|timme|timmar",
"minute": "minuter|minut|minuter",
"notifications": "Tysta notiser",
"specify_duration": "Specifiera tyst-varaktighet",
"title": "Tysta konto"
},
"show_reblogs": {
"cancel": "Avbryt",
"confirm": "Visa",
"description": "Är du säker på att du vill visa boosts från {0}?",
"title": "Visa boosts"
},
"unfollow": {
"cancel": "Avbryt",
"confirm": "Avfölj",
"description": "Är du säker på att du vill avfölja {0}?",
"title": "Avfölj"
}
},
"conversation": {
"with": "med"
},
"custom_cards": {
"stackblitz": {
"lines": "Rader {0}",
"open": "Öppna",
"snippet_from": "Urklipp från {0}"
}
},
"error": {
"account_not_found": "Konto {0} hittades ej",
"explore_list_empty": "Inget trendar just nu. Kolla igen senare!",
"file_size_cannot_exceed_n_mb": "Filstorleken kan ej överstiga {0}MB",
"sign_in_error": "Kan ej ansluta till servern.",
"status_not_found": "Inlägget hittades ej",
"unsupported_file_format": "Filformatet stödjs ej"
},
"help": {
"build_preview": {
"desc1": "Du använder just nu en förhandsversion av Elk från communityt - {0}.",
"desc2": "Den kan innehålla ogranskade eller till och med illvilliga ändringar.",
"desc3": "Logga inte in med ditt riktiga konto.",
"title": "Förhandsversion"
},
"desc_highlight": "Förvänta dig buggar här och där och funktioner som saknas.",
"desc_para1": "Elk är en smidig Mastodon-webbklient. Du kan logga in med ditt Mastodon-konto och använda det för att interagera med fediversumet.",
"desc_para2": "Elk är Open Source och vi förbättrar det aktivt som ett community-projekt. Gå med och låt oss bygga det tillsammans!",
"desc_para3": "För att accelerera utvecklingen så kan du sponsra teamet genom GitHub Sponsors. Vi hoppas du gillar Elk!",
"desc_para4": "Om du vill göra en buggrapport, hjälpa oss med testning, ge oss feedback eller bidra,",
"desc_para5": "nå ut till oss på GitHub",
"desc_para6": "och involvera dig.",
"footer_team": "Elk-teamet",
"title": "Välkommen till Elk!"
},
"language": {
"search": "Sök"
},
"list": {
"add_account": "Lägg till konto i listan",
"cancel_edit": "Avbryt redigering",
"clear_error": "Rensa fel",
"create": "Skapa",
"delete": "Radera denna lista",
"delete_error": "Ett fel uppstod när listan skulle raderas",
"edit": "Redigera denna lista",
"edit_error": "Ett fel uppstod när listan skulle uppdateras",
"error": "Ett fel uppstod när listan skulle skapas",
"error_prefix": "Fel: ",
"list_title_placeholder": "Listnamn",
"manage": "Hantera listor",
"modify_account": "Modifiera listor med konto",
"remove_account": "Radera konto från lista",
"save": "Spara ändringar",
"search_following_desc": "Sök efter personer du följer",
"search_following_placeholder": "Sök bland personer du följer"
},
"magic_keys": {
"dialog_header": "Kortkommandon",
"groups": {
"actions": {
"boost": "Boosta",
"command_mode": "Kommandoläge",
"compose": "Komponera",
"favourite": "Favoritmarkera",
"search": "Sök",
"show_new_items": "Visa nya objekt",
"title": "Åtgärder"
},
"media": {
"title": "Media"
},
"navigation": {
"go_to_bookmarks": "Bokmärken",
"go_to_conversations": "Konversationer",
"go_to_explore": "Upptäck",
"go_to_favourites": "Favoriter",
"go_to_federated": "Federerat",
"go_to_home": "Hem",
"go_to_lists": "Listor",
"go_to_local": "Lokalt",
"go_to_notifications": "Notiser",
"go_to_profile": "Profil",
"go_to_search": "Sök",
"go_to_settings": "Inställningar",
"next_status": "Nästa inlägg",
"previous_status": "Föregående inlägg",
"shortcut_help": "Kortkommandon",
"title": "Navigering"
}
},
"sequence_then": "then"
},
"menu": {
"add_personal_note": "Lägg till personlig anteckning för {0}",
"block_account": "Blockera {0}",
"block_domain": "Blockera domän {0}",
"copy_link_to_post": "Kopiera länk till detta inlägg",
"copy_original_link_to_post": "Kopiera originallänk till detta inlägg",
"delete": "Radera",
"delete_and_redraft": "Radera & gör om",
"direct_message_account": "Skicka meddelande till {0}",
"edit": "Redigera",
"hide_reblogs": "Göm boosts från {0}",
"mention_account": "Omnämn {0}",
"mute_account": "Tysta {0}",
"mute_conversation": "Tysta detta inlägg",
"open_in_original_site": "Öppna på originalsidan",
"pin_on_profile": "Fäst i profil",
"remove_personal_note": "Ta bort personlig anteckning från {0}",
"report_account": "Anmäl {0}",
"share_account": "Dela {0}",
"share_post": "Dela detta inlägg",
"show_favourited_and_boosted_by": "Visa vem som favoritmarkerade och boostade",
"show_reblogs": "Visa boosts från {0}",
"show_untranslated": "Visa oöversatta",
"toggle_theme": {
"dark": "Växla mörkt läge",
"light": "Växla ljust läge"
},
"translate_post": "Översätt inlägg",
"unblock_account": "Avblockera {0}",
"unblock_domain": "Avblockera domain {0}",
"unfollow_account": "Avfölj {0}",
"unmute_account": "Avtysta {0}",
"unmute_conversation": "Avtysta detta inlägg",
"unpin_on_profile": "Ångra fäst i profil"
},
"modals": {
"aria_label_close": "Stäng"
},
"nav": {
"back": "Gå tillbaka",
"blocked_domains": "Blockerade domäner",
"blocked_users": "Blockerade användare",
"bookmarks": "Bokmärken",
"built_at": "Byggd {0}",
"compose": "Komponera",
"conversations": "Konversationer",
"docs": "Dokumentation",
"explore": "Upptäck",
"favourites": "Favoriter",
"federated": "Federerat",
"hashtags": "Hashtaggar",
"home": "Hem",
"list": "Lista",
"lists": "Listor",
"local": "Lokalt",
"more_menu": "Mer",
"muted_users": "Tystade användare",
"notifications": "Notiser",
"privacy": "Integritet",
"profile": "Profil",
"search": "Sök",
"select_feature_flags": "Växla funktionsflaggor",
"select_font_size": "Teckenstorlek",
"select_language": "Språk för gränssnittet",
"settings": "Inställningar",
"show_intro": "Visa introduktion",
"toggle_theme": "Växla tema",
"zen_mode": "Zen-läge"
},
"notification": {
"and": "och",
"favourited_post": "favoritmarkerade ditt inlägg",
"followed_you": "följde dig",
"followed_you_count": "{0} följde dig|{0} följde dig|{0} följde dig",
"missing_type": "SAKNAS notification.type:",
"others": "{0} andra|{0} andra|{0} andra",
"reblogged_post": "boostade ditt inlägg",
"reported": "{0} anmälde {1}",
"request_to_follow": "begärde att följa dig",
"signed_up": "registrerade sig",
"update_status": "uppdaterade deras inlägg"
},
"placeholder": {
"content_warning": "Skriv din varning här",
"default_1": "Vad tänker du på?",
"reply_to_account": "Svara till {0}",
"replying": "Svarar"
},
"polls": {
"allow_multiple": "Tillåt flera val",
"cancel": "Avbryt",
"create": "Skapa omröstning",
"disallow_multiple": "Tillåt ej flera val",
"expiration": "Omröstning upphör",
"hide_votes": "Dölj totala röster till slutet",
"option_placeholder": "Omröstningsval {current}/{max}",
"remove_option": "Ta bort val",
"settings": "Omröstningsval",
"show_votes": "Visa alltid totalt antal röster"
},
"pwa": {
"dismiss": "Avvisa",
"install": "Installera",
"install_title": "Installera Elk",
"screenshots": {
"dark": "Skärmavbild av Elk i mörkt läge",
"light": "Skärmavbild av Elk i ljust läge"
},
"title": "Ny Elk-uppdatering tillgänglig!",
"update": "Uppdatera",
"update_available_short": "Uppdatera Elk",
"webmanifest": {
"canary": {
"description": "En smidig Mastodon-webbklient (canary)",
"name": "Elk (canary)",
"short_name": "Elk (canary)"
},
"dev": {
"description": "En smidig Mastodon-webbklient (dev)",
"name": "Elk (dev)",
"short_name": "Elk (dev)"
},
"preview": {
"description": "En smidig Mastodon-webbklient (preview)",
"name": "Elk (preview)",
"short_name": "Elk (preview)"
},
"release": {
"description": "En smidig Mastodon-webbklient",
"name": "Elk",
"short_name": "Elk"
}
}
},
"report": {
"additional_comments": "Övriga kommentarer",
"another_server": "Användaren du anmäler är från en annan server",
"anything_else": "Är det något annat vi borde veta om?",
"block_desc": "Du kommer inte att se inlägg från den här användaren i fortsättningen. De kommer inte kunna se dina inlägg eller följa dig. De kommer kunna se att de är blockerade.",
"dontlike": "Jag gillar det inte",
"dontlike_desc": "Det är inget jag vill se",
"forward": "Ja, vidarebefordra denna anmälan till {0}",
"forward_question": "Vill du även skicka en anonymiserad kopia av denna anmälan till servern?",
"further_actions": {
"limit": {
"description": "Här är dina alternativ för att kontrollera vad du ser:",
"title": "Vill du inte se det här?"
},
"report": {
"description": "Medan vi granskar detta, så kan du ta följande åtgärder:",
"title": "Tack för din anmälan, vi kommer ta en titt på detta."
}
},
"limiting": "Begränsar {0}",
"mute_desc": "Du kommer inte att se inlägg från den här användaren i fortsättningen. De kommer fortfarande kunna följa dig och se dina inlägg. De kommer inte veta att de är tystade.",
"other": "Det är något annat",
"other_desc": "Problemet passar inte i någon av kategorierna",
"reporting": "Anmäler {0}",
"select_many": "Välj allt som stämmer in:",
"select_one": "Välj den bästa matchningen:",
"select_posts": "Finns det några inlägg som styrker denna anmälan?",
"select_posts_other": "Finns det några andra inlägg som styrker denna anmälan?",
"spam": "Det är skräppost",
"spam_desc": "Skadliga länkar, klicklockeri eller upprepande svar",
"submit": "Skicka in anmälan",
"unfollow_desc": "Du kommer inte att se inlägg från den här användaren i ditt flöde i fortsättningen. Du kan dock komma att se inlägg från den här användaren på andra ställen.",
"violation": "Det bryter mot en eller flera av serverreglerna",
"whats_wrong_account": "Berätta vad som är fel med det här kontot",
"whats_wrong_post": "Berätta vad som är fel med det här inlägget"
},
"search": {
"search_desc": "Sök efter personer & hashtaggar",
"search_empty": "Kunde inte hitta något med dessa söktermer"
},
"settings": {
"about": {
"built_at": "Byggd",
"label": "Om",
"meet_the_team": "Möt teamet",
"sponsor_action": "Sponsra oss",
"sponsor_action_desc": "Sponsra teamet som utvecklar Elk",
"sponsors": "Sponsorer",
"sponsors_body_1": "Elk möjliggörs tack vare det generösa stödet och hjälpen från:",
"sponsors_body_2": "Samt alla företag och individer som stödjer Elk-teamet och dess medlemmar.",
"sponsors_body_3": "Om du tycker om appen, överväg då att sponsra oss:",
"version": "Version"
},
"account_settings": {
"description": "Ändra dina kontoinställningar i Mastodon-gränssnittet",
"label": "Kontoinställningar"
},
"interface": {
"bottom_nav": "Nedre navigeringsfält",
"bottom_nav_instructions": "Välj upp till fem stycken favoritknappar för det nedre navigeringsfältet. Måste innehålla \"Mer\"-knappen",
"color_mode": "Färgläge",
"dark_mode": "Mörkt",
"default": " (standard)",
"font_size": "Teckenstorlek",
"label": "Gränssnitt",
"light_mode": "Ljust",
"system_mode": "System",
"theme_color": "Färgtema"
},
"language": {
"display_language": "Språk för gränssnittet",
"how_to_contribute": "Hur kan du bidra?",
"label": "Språk",
"post_language": "Inläggsspråk",
"status": "Översättningsstatus: {0}/{1} ({2}%)",
"translations": {
"add": "Lägg till",
"choose_language": "Välj språk",
"heading": "Översättningar",
"hide_specific": "Dölj specifika översättningar",
"remove": "Ta bort"
}
},
"notifications": {
"label": "Notiser",
"notifications": {
"label": "Inställningar för notiser"
},
"push_notifications": {
"alerts": {
"favourite": "Favoriter",
"follow": "Nya följare",
"mention": "Omnämnanden",
"poll": "Omröstningar",
"reblog": "Boostar",
"title": "Vilka notiser vill jag ta emot?"
},
"description": "Ta emot notiser även när du inte använder Elk.",
"instructions": "Glöm inte att spara dina ändringar med @:settings.notifications.push_notifications.save_settings -knappen!",
"label": "Inställningar för push-notiser",
"policy": {
"all": "Från vem som helst",
"followed": "Från personer jag följer",
"follower": "Från personer som följer mig",
"none": "Från ingen",
"title": "Vem kan jag ta emot notiser från?"
},
"save_settings": "Spara inställningar",
"subscription_error": {
"clear_error": "Rensa fel",
"error_hint": "Du kan konsultera en lista med vanliga frågor för att lösa problem: {0}.",
"invalid_vapid_key": "Den allmänna nyckeln för VAPID verkar vara ogiltig.",
"permission_denied": "Åtkomst nekad: aktivera notiser i din webbläsare.",
"repo_link": "Elks förvar på GitHub",
"request_error": "Ett fel inträffade när prenumerationen förfrågades. Prova igen, och om felet återstår, vänligen anmäl felet till Elks förvar på Github.",
"title": "Kunde inte prenumerera på push-notiser",
"too_many_registrations": "På grund av begränsningar i webbläsaren, kan Elk inte använda tjänsten för push-notiser för flera konton på olika servrar. Du bör avprenumerera från push-notiser på ett annat konto och försök igen.",
"vapid_not_supported": "Din webbläsare stödjer Web Push Notifications, men verkar inte ha implementerat VAPID-protokollet."
},
"title": "Inställningar för push-notiser",
"undo_settings": "Ångra ändringar",
"unsubscribe": "Avaktivera push-notiser",
"unsupported": "Din webbläsare stödjer inte push-notiser.",
"warning": {
"enable_close": "Stäng",
"enable_description": "För att ta emot notiser när Elk inte är öppet, aktivera push-notiser. Du kan kontrollera precis vilka typer av interaktioner som ska generera push-notiser via \"@:settings.notifications.show_btn{'\"'} -knappen ovan när det är aktiverat.",
"enable_description_desktop": "För att ta emot notiser när Elk inte är öppet, aktivera push-notiser. Du kan kontrollera precis vilka typer av interaktioner som ska generera push-notiser via \"Inställningar > Notiser > Inställningar för push-notiser\" när det är aktiverat.",
"enable_description_mobile": "Du kan även komma åt inställningarna genom att använda navigationsmenyn \"Inställningar > Notiser > Inställningar för push-notiser\".",
"enable_description_settings": "För att ta emot notiser när Elk inte är öppet, aktivera push-notiser. Du kan kontrollera precis vilka typer av interaktioner som ska generera push-notiser på denna sida när du aktiverat dem.",
"enable_desktop": "Aktivera push-notiser",
"enable_title": "Missa aldrig något",
"re_auth": "Det ser ut som att din server inte stödjer push-notiser. Prova att logga ut och logga in igen, och om detta meddelande fortfarande dyker upp så kontakta din serveradministratör."
}
},
"show_btn": "Gå till inställningar för notiser",
"under_construction": "Under uppbyggnad"
},
"notifications_settings": "Notiser",
"preferences": {
"embedded_media": "Inbäddad mediaspelare",
"embedded_media_description": "Visa en inbäddad mediaspelare istället för den vanliga förhandsvisningskortet när delade medialänkar öppnas.",
"enable_autoplay": "Aktivera automatisk uppspelning",
"enable_data_saving": "Aktivera datasparläge",
"enable_data_saving_description": "Spara på din dataförbrukning genom att förhindra att bilagor laddas automatiskt.",
"enable_pinch_to_zoom": "Aktivera nyp för att zooma",
"github_cards": "GitHub-kort",
"github_cards_description": "När ett inlägg har en GitHub-länk, visas ett HTML-kort med hjälp av social graph-metadata istället för bilden.",
"grayscale_mode": "Gråskaleläge",
"hide_account_hover_card": "Dölj konto-hoverkort",
"hide_alt_indi_on_posts": "Dölj alt-indikator på inlägg",
"hide_boost_count": "Dölj boost-räknare",
"hide_favorite_count": "Dölj favoriträknare",
"hide_follower_count": "Dölj följer/följarräknare",
"hide_gif_indi_on_posts": "Dölj gif-indikator på inlägg",
"hide_news": "Dölj nyheter",
"hide_reply_count": "Dölj svarsräknare",
"hide_tag_hover_card": "Dölj tagg-hoverkort",
"hide_translation": "Dölj översättningar",
"hide_username_emojis": "Dölj användarnamn-emojis",
"hide_username_emojis_description": "Döljer emojis på användarnamn i tidslinjer. Emojis kommer fortfarande vara synliga i deras profiler.",
"label": "Inställningar",
"optimize_for_low_performance_device": "Optimera för enhet med låg prestanda",
"title": "Experimentella funktioner",
"unmute_videos": "Videoljud på som standard",
"use_star_favorite_icon": "Använd stjärna som favoritikon",
"user_picker": "Användarväljare",
"user_picker_description": "Visar alla profilbilder för inloggade konton i det nedre vänstra hörnet så att du snabbt kan växla mellan dem.",
"virtual_scroll": "Virtuell skrollning",
"virtual_scroll_description": "Använder en virtuell lista i tidslinjer så att ett större antal objekt kan renderas följsamt.",
"wellbeing": "Välbefinnande",
"zen_mode": "Zen-läge",
"zen_mode_description": "Döljer flikar så länge muspekaren inte är över dem. Döljer även vissa element från tidslinjen."
},
"profile": {
"appearance": {
"bio": "Biografi",
"description": "Ändra profilbild, användarnamn, profil, etc.",
"display_name": "Visningsnamn",
"label": "Utseende",
"profile_metadata": "Profil-metadata",
"profile_metadata_desc": "Du kan ha upp till {0} objekt för visning som en tabell på din profil",
"profile_metadata_label": "Etikett",
"profile_metadata_value": "Innehåll",
"title": "Redigera profil"
},
"featured_tags": {
"description": "Folk kan bläddra bland dina offentliga inlägg under dessa hashtaggar.",
"label": "Utvalda hashtaggar",
"under_construction": "Under uppbyggnad"
},
"label": "Profil"
},
"select_a_settings": "Välj en inställning",
"users": {
"export": "Exportera användarnycklar",
"import": "Importera användarnycklar",
"label": "Inloggade användare"
}
},
"share_target": {
"description": "Elk kan konfigureras så att du kan dela innehåll från andra applikationer. Installera Elk på din enhet eller dator och logga in.",
"hint": "För att kunna dela innehåll med Elk så måste Elk vara installerat och du måste vara inloggad.",
"title": "Dela med Elk"
},
"state": {
"attachments_exceed_server_limit": "Antalet bilagor överstiger gränsen per inlägg.",
"attachments_limit_audio_error": "Maximal ljudfilsstorlek har överskridits: {0}",
"attachments_limit_error": "Gränsen per inlägg har överstigits",
"attachments_limit_image_error": "Maximal bildstorlek har överskridits: {0}",
"attachments_limit_unknown_error": "Maximal filstorlek har överskridits: {0}",
"attachments_limit_video_error": "Maximal videostorlek har överskridits: {0}",
"edited": "(Redigerad)",
"editing": "Redigerar",
"loading": "Laddar...",
"publish_failed": "Publicering misslyckades",
"publishing": "Publicerar",
"save_failed": "Sparningen misslyckades",
"upload_failed": "Uppladdning misslyckades",
"uploading": "Laddar upp..."
},
"status": {
"account": {
"suspended_message": "Kontot för detta inlägg har stängts av.",
"suspended_show": "Visa innehållet ändå?"
},
"boosted_by": "Boostad av",
"edited": "Redigerad {0}",
"embedded_warning": "Om du spelar detta så kan det avslöja din IP-adress för andra.",
"favourited_by": "Favoritmarkerad av",
"filter_hidden_phrase": "Filtrerad av",
"filter_show_anyway": "Visa ändå",
"gif": "GIF",
"img_alt": {
"ALT": "ALT",
"desc": "Beskrivning",
"dismiss": "Stäng",
"read": "Läs {0} beskrivning"
},
"pinned": "Fästa inlägg",
"poll": {
"count": "{0} röster|{0} röst|{0} röster",
"ends": "slutar {0}",
"finished": "avslutad {0}",
"update": "Uppdatera omröstning"
},
"replying_to": "Svarar till {0}",
"show_full_thread": "Visa hela tråden",
"someone": "någon",
"spoiler_media_hidden": "Media dold",
"spoiler_show_less": "Visa mindre",
"spoiler_show_more": "Visa mer",
"thread": "Tråd",
"try_original_site": "Försök på originalsidan"
},
"status_history": {
"created": "skapades {0}",
"edited": "redigerades {0}"
},
"tab": {
"accounts": "Konton",
"for_you": "För dig",
"hashtags": "Hashtaggar",
"list": "Lista",
"media": "Media",
"news": "Nyheter",
"notifications_admin": {
"report": "Anmäl",
"sign_up": "Registrera"
},
"notifications_all": "Alla",
"notifications_favourite": "Favoritmarkering",
"notifications_follow": "Följ",
"notifications_follow_request": "Följförfrågan",
"notifications_mention": "Omnämnanden",
"notifications_more_tooltip": "Filtrera notiser efter typ",
"notifications_poll": "Omröstning",
"notifications_reblog": "Boost",
"notifications_status": "Status",
"notifications_update": "Uppdatering",
"posts": "Inlägg",
"posts_with_replies": "Inlägg & svar"
},
"tag": {
"follow": "Följ",
"follow_label": "Följ {0}-taggen",
"unfollow": "Avfölj",
"unfollow_label": "Avfölj {0}-taggen"
},
"time_ago_options": {
"day_future": "om 0 dagar|imorgon|om {n} dagar",
"day_past": "0 dagar sedan|igår|{n} dagar sedan",
"hour_future": "om 0 timmar|om 1 timme|om {n} timmar",
"hour_past": "0 timmar sedan|1 timme sedan|{n} timmar sedan",
"just_now": "just nu",
"minute_future": "om 0 minuter|om 1 minut|om {n} minuter",
"minute_past": "0 minuter sedan|1 minut sedan|{n} minuter sedan",
"month_future": "om 0 månader|nästa månad|om {n} månader",
"month_past": "0 månader sedan|förra månaden|{n} månader sedan",
"second_future": "just nu|om {n} sekund|om {n} sekunder",
"second_past": "just nu|{n} sekund sedan|{n} sekunder sedan",
"short_day_future": "om {n}d",
"short_day_past": "{n}d",
"short_hour_future": "om {n}h",
"short_hour_past": "{n}h",
"short_minute_future": "om {n}min",
"short_minute_past": "{n}min",
"short_month_future": "om {n}mån",
"short_month_past": "{n}mån",
"short_second_future": "om {n}s",
"short_second_past": "{n}s",
"short_week_future": "om {n}v",
"short_week_past": "{n}v",
"short_year_future": "om {n}år",
"short_year_past": "{n}år",
"week_future": "om 0 veckor|nästa vecka|om {n} veckor",
"week_past": "0 veckor sedan|förra veckan|{n} veckor sedan",
"year_future": "om 0 år|nästa år|om {n} år",
"year_past": "0 år sedan|förra året|{n} år sedan"
},
"timeline": {
"no_posts": "Inga inlägg här!",
"show_new_items": "Visa {v} nya inlägg|Visa {v} nytt inlägg|Visa {v} nya inlägg",
"view_older_posts": "Äldre inlägg från andra instanser kanske inte visas."
},
"title": {
"federated_timeline": "Federerad tidslinje",
"local_timeline": "Lokal tidslinje"
},
"tooltip": {
"add_content_warning": "Lägg till innehållsvarning",
"add_emojis": "Lägg till emojis",
"add_media": "Lägg till bilder, video eller en ljudfil",
"add_publishable_content": "Lägg till innehåll för publicering",
"add_thread_item": "Lägg till inlägg i tråden",
"change_content_visibility": "Ändra innehållets synlighet",
"change_language": "Byt språk",
"emoji": "Emoji",
"explore_links_intro": "Dessa nyheter pratas det om just nu på denna och andra servrar på det decentraliserade nätverket.",
"explore_posts_intro": "Dessa inlägg från denna och andra servrar på det decentraliserade nätverket har fått fäste på denna server just nu.",
"explore_tags_intro": "Dessa hashtaggar har fått fäste bland folk på denna och andra servrar på det decentraliserade nätverket",
"open_editor_tools": "Redigeringsverktyg",
"pick_an_icon": "Välj en ikon",
"publish_failed": "Stäng misslyckade meddelanden på toppen av redigeringsverktyget för att åter publicera inläggen",
"remove_thread_item": "Ta bort inlägg från tråden",
"start_thread": "Starta tråd",
"toggle_bold": "Fet",
"toggle_code_block": "Kodblock",
"toggle_italic": "Kursiv"
},
"user": {
"add_existing": "Lägg till ett befintligt konto",
"server_address_label": "Mastodon-serveradress",
"sign_in_desc": "Logga in för att följa profiler eller hashtaggar samt favoritmarkera, dela och svara på inlägg, eller interagera från ditt konto på en annan server.",
"sign_in_notice_title": "Visar {0} offentliga data",
"sign_out_account": "Logga ut {0}",
"single_instance_sign_in_desc": "Logga in för att följa profiler eller hashtaggar, favoritmarkera, dela och svara på inlägg",
"tip_no_account": "Om du inte har ett Mastodon-konto än, {0}.",
"tip_register_account": "välj server och registrera ett"
},
"visibility": {
"direct": "Privat omnämnande",
"direct_desc": "Synligt endast för omnämnda användare",
"private": "Endast följare",
"private_desc": "Synligt endast för följare",
"public": "Offentlig",
"public_desc": "Synligt för alla",
"unlisted": "Offentlig (begränsad)",
"unlisted_desc": "Synligt för alla, men bortvald från upptäcktsfunktioner"
}
}

View file

@ -1,5 +1,5 @@
import type { BuildInfo } from '#shared/types' import type { BuildInfo } from '../shared/types'
import { createResolver, defineNuxtModule } from '@nuxt/kit' import { createResolver, defineNuxtModule } from 'nuxt/kit'
import { isCI } from 'std-env' import { isCI } from 'std-env'
import { getEnv, version } from '../config/env' import { getEnv, version } from '../config/env'

View file

@ -1,5 +1,5 @@
import { lstat } from 'node:fs' import { lstat } from 'node:fs'
import { createResolver, defineNuxtModule } from '@nuxt/kit' import { createResolver, defineNuxtModule } from 'nuxt/kit'
import { currentLocales } from '../config/i18n' import { currentLocales } from '../config/i18n'
const virtual = 'virtual:emoji-mart-lang-importer' const virtual = 'virtual:emoji-mart-lang-importer'

View file

@ -1,5 +1,5 @@
import { addVitePlugin, defineNuxtModule } from '@nuxt/kit'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { addVitePlugin, defineNuxtModule } from 'nuxt/kit'
export default defineNuxtModule({ export default defineNuxtModule({
meta: { meta: {

View file

@ -1,7 +1,7 @@
import type { ManifestOptions } from 'vite-plugin-pwa' import type { ManifestOptions } from 'vite-plugin-pwa'
import { Buffer } from 'node:buffer' import { Buffer } from 'node:buffer'
import { readFile } from 'node:fs/promises' import { readFile } from 'node:fs/promises'
import { createResolver } from '@nuxt/kit' import { createResolver } from 'nuxt/kit'
import { THEME_COLORS } from '../../app/constants/index' import { THEME_COLORS } from '../../app/constants/index'
import { getEnv } from '../../config/env' import { getEnv } from '../../config/env'
import { currentLocales } from '../../config/i18n' import { currentLocales } from '../../config/i18n'

View file

@ -6,7 +6,7 @@ import type { VitePWANuxtOptions } from './types'
import { mkdir, readFile, writeFile } from 'node:fs/promises' import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname } from 'node:path' import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { addPlugin, createResolver, defineNuxtModule } from '@nuxt/kit' import { addPlugin, createResolver, defineNuxtModule } from 'nuxt/kit'
import { join, resolve } from 'pathe' import { join, resolve } from 'pathe'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import { configurePWAOptions } from './config' import { configurePWAOptions } from './config'

View file

@ -1,68 +0,0 @@
import { rm } from 'node:fs/promises'
import { addImports, addImportsSources, addPlugin, createResolver, defineNuxtModule, useNuxt } from '@nuxt/kit'
import { resolveModulePath } from 'exsolve'
const mockProxy = resolveModulePath('mocked-exports/proxy', { from: import.meta.url })
export default defineNuxtModule({
meta: {
name: 'tauri',
},
setup() {
const nuxt = useNuxt()
const { resolve } = createResolver(import.meta.url)
if (!process.env.TAURI_PLATFORM)
return
if (nuxt.options.dev)
nuxt.options.ssr = false
nuxt.options.pwa.disable = true
nuxt.options.sourcemap.client = false
nuxt.options.alias = {
...nuxt.options.alias,
'unstorage/drivers/fs': mockProxy,
'unstorage/drivers/cloudflare-kv-http': mockProxy,
'#storage-config': resolve('./runtime/storage-config'),
'node:events': 'unenv/runtime/node/events/index',
'#build-info': resolve('./runtime/build-info'),
}
nuxt.hook('vite:extend', ({ config }) => {
config.build!.target = ['chrome100', 'safari15']
config.envPrefix = [...config.envPrefix || [], 'VITE_', 'TAURI_']
})
// prevent creation of server routes
nuxt.hook('nitro:config', (config) => {
config.srcDir = './_nonexistent'
config.scanDirs = []
})
addImportsSources({
from: 'h3',
imports: ['defineEventHandler', 'getQuery', 'getRouterParams', 'readBody', 'sendRedirect'] as Array<keyof typeof import('h3')>,
})
nuxt.options.imports.dirs = nuxt.options.imports.dirs || []
nuxt.options.imports.dirs.push(resolve('../../server/utils'))
addImports({ name: 'useStorage', from: resolve('./runtime/storage') })
addPlugin(resolve('./runtime/logging.client'))
addPlugin(resolve('./runtime/nitro.client'))
// cleanup files copied from the public folder that we don't need
nuxt.hook('close', async () => {
await rm('.output/public/_redirects')
await rm('.output/public/apple-touch-icon.png')
await rm('.output/public/elk-og.png')
await rm('.output/public/favicon.ico')
await rm('.output/public/pwa-192x192.png')
await rm('.output/public/pwa-512x512.png')
await rm('.output/public/robots.txt')
})
},
})

View file

@ -1 +0,0 @@
export const env = useAppConfig().env

View file

@ -1,18 +0,0 @@
import * as log from 'tauri-plugin-log-api'
// When running inside Tauri, catch all logs from 3rd party packages and direct them to the unified logging stream
export default defineNuxtPlugin(() => {
// eslint-disable-next-line no-global-assign
console = {
...console,
trace: log.trace,
debug: log.debug,
log: log.info,
warn: log.warn,
error: log.error,
}
window.addEventListener('unhandledrejection', err =>
log.error(err.reason))
window.addEventListener('error', err => log.error(err.error), true)
})

View file

@ -1,73 +0,0 @@
import type { FetchResponse } from 'ofetch'
import {
createApp,
createRouter,
defineLazyEventHandler,
toNodeListener,
} from 'h3'
import { fetchNodeRequestHandler } from 'node-mock-http'
import { createFetch } from 'ofetch'
const handlers = [
{
route: '/api/:server/oauth',
handler: defineLazyEventHandler(() => import('~~/server/api/[server]/oauth/[origin]').then(r => r.default || r)),
},
{
route: '/api/:server/login',
handler: defineLazyEventHandler(() => import('~~/server/api/[server]/login').then(r => r.default || r)),
},
{
route: '/api/list-servers',
handler: defineLazyEventHandler(() => import('~~/server/api/list-servers').then(r => r.default || r)),
},
]
// @ts-expect-error undeclared global window property
window.__NUXT__.config = {
// @ts-expect-error undeclared global window property
...window.__NUXT__.config,
storage: {},
}
export default defineNuxtPlugin(async () => {
const config = useRuntimeConfig()
const h3App = createApp({
debug: import.meta.dev,
// TODO: add global error handler
// onError: (err, event) => {
// console.log({ err, event })
// },
})
const router = createRouter()
for (const h of handlers)
router.use(h.route, h.handler)
// @ts-expect-error TODO: fix
h3App.use(config.app.baseURL, router)
const nodeHandler = toNodeListener(h3App)
const localFetch: typeof fetch = async (input, init) => {
if (!input.toString().startsWith('/')) {
return globalThis.fetch(input.toString(), init)
}
return await fetchNodeRequestHandler(nodeHandler, input.toString(), init)
}
// @ts-expect-error error types are subtly different here in a future nitro version
globalThis.$fetch = createFetch({
fetch: localFetch,
Headers,
defaults: { baseURL: config.app.baseURL },
})
const route = useRoute()
if (route.path.startsWith('/api')) {
const result = (await ($fetch.raw as any)(route.fullPath)) as FetchResponse<unknown>
if (result.headers.get('location'))
location.href = result.headers.get('location')!
}
})

View file

@ -1,2 +0,0 @@
export const driver = undefined
export const fsBase = ''

View file

@ -1,6 +1,6 @@
import type { BuildInfo } from './shared/types' import type { BuildInfo } from './shared/types'
import { createResolver, useNuxt } from '@nuxt/kit'
import { resolveModulePath } from 'exsolve' import { resolveModulePath } from 'exsolve'
import { createResolver, useNuxt } from 'nuxt/kit'
import { isCI, isDevelopment, isWindows } from 'std-env' import { isCI, isDevelopment, isWindows } from 'std-env'
import { isPreview } from './config/env' import { isPreview } from './config/env'
import { currentLocales } from './config/i18n' import { currentLocales } from './config/i18n'
@ -11,12 +11,10 @@ const { resolve } = createResolver(import.meta.url)
const mockProxy = resolveModulePath('mocked-exports/proxy', { from: import.meta.url }) const mockProxy = resolveModulePath('mocked-exports/proxy', { from: import.meta.url })
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2024-09-11', compatibilityDate: '2025-07-11',
future: {
compatibilityVersion: 4,
},
typescript: { typescript: {
tsConfig: { tsConfig: {
include: ['../tests/nuxt'],
exclude: ['../service-worker'], exclude: ['../service-worker'],
compilerOptions: { compilerOptions: {
// TODO: enable this once we fix the issues // TODO: enable this once we fix the issues
@ -37,12 +35,6 @@ export default defineNuxtConfig({
'@unlazy/nuxt', '@unlazy/nuxt',
'@nuxt/test-utils/module', '@nuxt/test-utils/module',
...(isDevelopment || isWindows) ? [] : ['nuxt-security'], ...(isDevelopment || isWindows) ? [] : ['nuxt-security'],
'~~/modules/emoji-mart-translation',
'~~/modules/purge-comments',
'~~/modules/build-env',
'~~/modules/tauri/index',
'~~/modules/pwa/index', // change to '@vite-pwa/nuxt' once released and remove pwa module
'stale-dep/nuxt',
], ],
vue: { vue: {
propsDestructure: true, propsDestructure: true,
@ -62,6 +54,9 @@ export default defineNuxtConfig({
experimental: { experimental: {
payloadExtraction: false, payloadExtraction: false,
renderJsonPayloads: true, renderJsonPayloads: true,
// Temporary workaround to avoid hash mismatch issue
// ref. https://github.com/elk-zone/elk/issues/3385#issuecomment-3335167005
entryImportMap: false,
}, },
css: [ css: [
'@unocss/reset/tailwind.css', '@unocss/reset/tailwind.css',
@ -69,9 +64,7 @@ export default defineNuxtConfig({
'~/styles/default-theme.css', '~/styles/default-theme.css',
'~/styles/vars.css', '~/styles/vars.css',
'~/styles/global.css', '~/styles/global.css',
...process.env.TAURI_PLATFORM === 'macos' '~/styles/scrollbars.css',
? []
: ['~/styles/scrollbars.css'],
'~/styles/tiptap.css', '~/styles/tiptap.css',
'~/styles/dropdown.css', '~/styles/dropdown.css',
], ],
@ -318,48 +311,19 @@ export default defineNuxtConfig({
colorMode: { classSuffix: '' }, colorMode: { classSuffix: '' },
i18n: { i18n: {
locales: currentLocales, locales: currentLocales,
lazy: true,
strategy: 'no_prefix', strategy: 'no_prefix',
detectBrowserLanguage: false, detectBrowserLanguage: false,
// relative to i18n dir on rootDir: not yet v4 compat layout // relative to i18n dir on rootDir: not yet v4 compat layout
langDir: '../locales', langDir: '../locales',
defaultLocale: 'en-US', defaultLocale: 'en-US',
experimental: {
generatedLocaleFilePathFormat: 'relative',
},
vueI18n: '../config/i18n.config.ts', vueI18n: '../config/i18n.config.ts',
bundle: {
optimizeTranslationDirective: false,
},
}, },
pwa, pwa,
staleDep: {
packageManager: 'pnpm',
},
unlazy: { unlazy: {
ssr: false, ssr: false,
}, },
}) })
declare global {
// eslint-disable-next-line ts/no-namespace
namespace NodeJS {
interface Process {
mock?: Record<string, any>
}
}
}
declare module '#app' {
interface PageMeta {
wideLayout?: boolean
}
interface RuntimeNuxtHooks {
'elk-logo:click': () => void
}
}
declare module '@nuxt/schema' { declare module '@nuxt/schema' {
interface AppConfig { interface AppConfig {
storage: any storage: any

View file

@ -1,52 +1,58 @@
{ {
"name": "@elk-zone/elk", "name": "@elk-zone/elk",
"type": "module", "type": "module",
"version": "0.16.0", "version": "0.17.0",
"packageManager": "pnpm@9.15.9", "packageManager": "pnpm@10.23.0",
"license": "MIT", "license": "MIT",
"homepage": "https://elk.zone/", "homepage": "https://elk.zone/",
"main": "./nuxt.config.ts", "main": "./nuxt.config.ts",
"scripts": { "scripts": {
"build": "nuxi build", "build": "nuxt build",
"dev": "nuxi dev --port 5314", "dev": "nuxt dev --port 5314",
"dev:pwa": "VITE_DEV_PWA=true nuxi dev --port 5314", "dev:pwa": "VITE_DEV_PWA=true nuxt dev --port 5314",
"dev:mocked": "nuxi dev --port 5314 --dotenv .env.mock", "dev:mocked": "nuxt dev --port 5314 --dotenv .env.mock",
"dev:mocked:pwa": "VITE_DEV_PWA=true nuxi dev --port 5314 --dotenv .env.mock", "dev:mocked:pwa": "VITE_DEV_PWA=true nuxt dev --port 5314 --dotenv .env.mock",
"dev:mocked:pwa:ssl": "VITE_DEV_PWA=true nuxi dev --port 5314 --https --ssl-cert ./https-dev-config/localhost.crt --ssl-key ./https-dev-config/localhost.key --dotenv .env.mock", "dev:mocked:pwa:ssl": "VITE_DEV_PWA=true nuxt dev --port 5314 --https --ssl-cert ./https-dev-config/localhost.crt --ssl-key ./https-dev-config/localhost.key --dotenv .env.mock",
"start": "PORT=5314 node .output/server/index.mjs", "start": "PORT=5314 node .output/server/index.mjs",
"start:https": "PORT=5314 node ./https-dev-config/local-https-server.mjs", "start:https": "PORT=5314 node ./https-dev-config/local-https-server.mjs",
"lint": "eslint --cache .", "lint": "eslint --cache .",
"lint:fix": "eslint --cache --fix .", "lint:fix": "eslint --cache --fix .",
"typecheck": "stale-dep && nuxi typecheck", "typecheck": "nuxt typecheck",
"prepare": "ignore-dependency-scripts \"tsx scripts/prepare.ts\"", "prepare": "node scripts/prepare-translation-status.ts && node scripts/prepare.ts",
"generate": "nuxi generate", "generate": "nuxt generate",
"test:unit": "stale-dep && vitest", "test:unit": "vitest",
"test:unit:ci": "stale-dep && vitest run", "test:unit:ci": "vitest run",
"test:typecheck": "stale-dep && vue-tsc --noEmit && vue-tsc --noEmit --project service-worker/tsconfig.json", "test:typecheck": "vue-tsc -b --noEmit",
"test": "nr test:unit", "test": "vitest",
"test:ci": "nr test:unit:ci", "test:ci": "vitest run",
"update:team:avatars": "tsx scripts/avatars.ts", "update:team:avatars": "node scripts/avatars.ts",
"cleanup-translations": "tsx scripts/cleanup-translations.ts", "cleanup-translations": "node scripts/cleanup-translations.ts",
"prepare-translation-status": "tsx scripts/prepare-translation-status.ts", "prepare-translation-status": "node scripts/prepare-translation-status.ts",
"generate-pwa-icons": "tsx scripts/generate-pwa-icons.ts", "generate-pwa-icons": "node scripts/generate-pwa-icons.ts",
"postinstall": "ignore-dependency-scripts \"stale-dep -u && simple-git-hooks && nuxi prepare && nr prepare-translation-status\"", "postinstall": "simple-git-hooks && nuxt prepare",
"release": "stale-dep && bumpp && tsx scripts/release.ts" "release": "bumpp && node scripts/release.ts"
}, },
"dependencies": { "dependencies": {
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
"@fnando/sparkline": "^0.3.10", "@fnando/sparkline": "^0.3.10",
"@iconify-emoji/twemoji": "^1.0.2", "@iconify-emoji/twemoji": "^1.0.2",
"@iconify/json": "^2.2.170", "@iconify/json": "^2.2.170",
"@iconify/utils": "^2.1.22", "@iconify/utils": "^3.0.0",
"@nuxt/devtools": "^2.4.1", "@intlify/core-base": "^11.1.12",
"@nuxt/test-utils": "^3.19.0", "@intlify/message-compiler": "^11.1.12",
"@intlify/shared": "^11.1.12",
"@nuxt/devtools": "^2.6.5",
"@nuxt/test-utils": "^3.19.2",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "^9.5.4", "@nuxtjs/i18n": "^10.1.0",
"@pinia/nuxt": "^0.11.0", "@pinia/nuxt": "^0.11.2",
"@tiptap/core": "2.2.4", "@tiptap/core": "2.2.4",
"@tiptap/extension-bold": "2.2.4", "@tiptap/extension-bold": "2.2.4",
"@tiptap/extension-character-count": "2.2.4", "@tiptap/extension-character-count": "2.2.4",
"@tiptap/extension-code": "2.2.4",
"@tiptap/extension-code-block": "2.2.4", "@tiptap/extension-code-block": "2.2.4",
"@tiptap/extension-document": "2.2.4",
"@tiptap/extension-hard-break": "2.2.4",
"@tiptap/extension-history": "2.2.4", "@tiptap/extension-history": "2.2.4",
"@tiptap/extension-italic": "2.2.4", "@tiptap/extension-italic": "2.2.4",
"@tiptap/extension-mention": "2.2.4", "@tiptap/extension-mention": "2.2.4",
@ -57,16 +63,19 @@
"@tiptap/starter-kit": "2.2.4", "@tiptap/starter-kit": "2.2.4",
"@tiptap/suggestion": "2.2.4", "@tiptap/suggestion": "2.2.4",
"@tiptap/vue-3": "2.2.4", "@tiptap/vue-3": "2.2.4",
"@unocss/nuxt": "^66.1.2", "@unhead/schema": "^2.0.17",
"@unlazy/nuxt": "^0.12.4",
"@unocss/nuxt": "^66.5.2",
"@upstash/redis": "^1.27.1", "@upstash/redis": "^1.27.1",
"@vercel/kv": "^3.0.0", "@vercel/kv": "^3.0.0",
"@vue-macros/nuxt": "^1.11.12", "@vue-macros/nuxt": "^1.13.5",
"@vueuse/core": "^12.0.0", "@vueuse/core": "^12.0.0",
"@vueuse/gesture": "^2.0.0", "@vueuse/gesture": "^2.0.0",
"@vueuse/integrations": "^12.0.0", "@vueuse/integrations": "^12.0.0",
"@vueuse/math": "^12.0.0", "@vueuse/math": "^12.0.0",
"@vueuse/motion": "2.2.6", "@vueuse/motion": "2.2.6",
"@vueuse/nuxt": "^13.2.0", "@vueuse/nuxt": "^13.9.0",
"@vueuse/shared": "^13.9.0",
"blurhash": "^2.0.5", "blurhash": "^2.0.5",
"browser-fs-access": "^0.38.0", "browser-fs-access": "^0.38.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
@ -79,47 +88,53 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"github-reserved-names": "^2.0.4", "github-reserved-names": "^2.0.4",
"happy-dom": "^16.0.0", "happy-dom": "^20.0.0",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"ignore-dependency-scripts": "^1.0.1",
"ioredis": "^5.7.0",
"iso-639-1": "^3.0.0", "iso-639-1": "^3.0.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lru-cache": "^11.0.0", "lru-cache": "^11.0.0",
"magic-string": "^0.30.19",
"masto": "^6.10.4", "masto": "^6.10.4",
"mocked-exports": "^0.1.1", "mocked-exports": "^0.1.1",
"node-emoji": "^2.1.3", "node-emoji": "^2.1.3",
"node-mock-http": "^1.0.0", "nuxt": "^4.1.2",
"nuxt-security": "^2.2.0", "nuxt-security": "^2.4.0",
"page-lifecycle": "^0.1.2", "page-lifecycle": "^0.1.2",
"pinia": "^3.0.2", "pathe": "^2.0.3",
"pinia": "^3.0.3",
"postcss-nested": "^7.0.0", "postcss-nested": "^7.0.0",
"prosemirror-highlight": "^0.13.0", "prosemirror-highlight": "^0.13.0",
"prosemirror-state": "^1.4.3",
"rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-node-polyfills": "^0.2.1",
"shiki": "^1.22.2", "shiki": "^1.22.2",
"simple-git": "^3.19.1", "simple-git": "^3.19.1",
"slimeform": "^0.10.0", "slimeform": "^0.10.0",
"stale-dep": "^0.8.0",
"std-env": "^3.7.0", "std-env": "^3.7.0",
"string-length": "^5.0.1", "string-length": "^5.0.1",
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log",
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store",
"theme-vitesse": "^0.8.0", "theme-vitesse": "^0.8.0",
"tiny-decode": "^0.1.3", "tiny-decode": "^0.1.3",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"ufo": "^1.5.3", "ufo": "^1.5.3",
"ultrahtml": "^1.5.3", "ultrahtml": "^1.5.3",
"unimport": "^3.10.0", "unimport": "^3.10.0",
"unstorage": "^1.17.1",
"vite": "^7.1.7",
"vite-plugin-pwa": "^0.21.0", "vite-plugin-pwa": "^0.21.0",
"vue": "^3.5.4",
"vue-advanced-cropper": "^2.8.9", "vue-advanced-cropper": "^2.8.9",
"vue-i18n": "^11.1.12",
"vue-virtual-scroller": "2.0.0-beta.8", "vue-virtual-scroller": "2.0.0-beta.8",
"workbox-build": "^7.1.1", "workbox-build": "^7.1.1",
"workbox-cacheable-response": "^7.1.0",
"workbox-expiration": "^7.1.0",
"workbox-precaching": "^7.1.0",
"workbox-routing": "^7.1.0",
"workbox-strategies": "^7.1.0",
"workbox-window": "^7.1.0", "workbox-window": "^7.1.0",
"ws": "^8.15.1" "ws": "^8.15.1"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^5.1.0", "@antfu/eslint-config": "^6.2.0",
"@antfu/ni": "^24.4.0",
"@types/chroma-js": "^3.1.1", "@types/chroma-js": "^3.1.1",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/fnando__sparkline": "^0.3.7", "@types/fnando__sparkline": "^0.3.7",
@ -127,39 +142,31 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/wicg-file-system-access": "^2023.10.6", "@types/wicg-file-system-access": "^2023.10.6",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@unlazy/nuxt": "^0.12.4", "@unocss/eslint-config": "^66.4.2",
"@unocss/eslint-config": "^66.4.1",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"bumpp": "^10.2.2", "bumpp": "^10.2.3",
"consola": "^3.4.2", "consola": "^3.4.2",
"eslint": "^9.32.0", "eslint": "^9.39.1",
"eslint-plugin-format": "^1.0.1", "eslint-plugin-format": "^1.0.1",
"flat": "^6.0.1", "flat": "^6.0.1",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.1",
"lint-staged": "^15.5.2", "lint-staged": "^16.1.6",
"nuxt": "^3.18.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sharp": "^0.34.3", "sharp": "^0.34.3",
"sharp-ico": "^0.1.5", "sharp-ico": "^0.1.5",
"simple-git-hooks": "^2.13.1", "simple-git-hooks": "^2.13.1",
"tsx": "^4.20.3",
"typescript": "^5.4.4", "typescript": "^5.4.4",
"vitest": "3.2.4", "vitest": "4.0.9",
"vue-tsc": "^2.1.6" "vue-tsc": "^2.1.6"
}, },
"pnpm": {
"patchedDependencies": {
"pinceau": "patches/pinceau.patch"
}
},
"resolutions": { "resolutions": {
"nuxt-component-meta": "0.13.0", "nuxt-component-meta": "0.14.2",
"unstorage": "^1.16.1", "unstorage": "^1.17.3",
"vitest": "3.2.4", "vitest": "4.0.9",
"vue": "^3.5.4" "vue": "^3.5.4"
}, },
"simple-git-hooks": { "simple-git-hooks": {
"pre-commit": "pnpm lint-staged" "pre-commit": "npx lint-staged"
}, },
"lint-staged": { "lint-staged": {
"*": "eslint --fix" "*": "eslint --fix"

View file

View file

@ -1,13 +0,0 @@
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 612f1c7908c2e973870be08c6fba1515e6e2b9ca..445a3b0574c5388b624d537fc17cbf4a08973ded 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -115,7 +115,7 @@ interface ModuleHooks {
interface ModuleOptions extends PinceauOptions {
}
-declare module '@vue/runtime-core' {
+declare module 'vue' {
interface ComponentCustomProperties {
$dt: DtFunction;
$pinceau: ComputedRef<string>;

File diff suppressed because it is too large Load diff

View file

@ -1,2 +1,21 @@
packages: packages:
- docs - docs
ignoreWorkspaceRootCheck: true
ignoredBuiltDependencies:
- '@parcel/watcher'
- '@tailwindcss/oxide'
- esbuild
- vue-demi
onlyBuiltDependencies:
- better-sqlite3
- sharp
- simple-git-hooks
packageManagerStrict: false
shellEmulator: true
verifyDepsBeforeRun: install

View file

@ -2,7 +2,7 @@ import { writeFile } from 'node:fs/promises'
import fs from 'fs-extra' import fs from 'fs-extra'
import { ofetch } from 'ofetch' import { ofetch } from 'ofetch'
import { join, resolve } from 'pathe' import { join, resolve } from 'pathe'
import { elkTeamMembers } from '../app/composables/about' import { elkTeamMembers } from '../app/composables/about.ts'
const avatarsDir = resolve('./public/avatars/') const avatarsDir = resolve('./public/avatars/')

View file

@ -1,8 +1,8 @@
import { Buffer } from 'node:buffer' import { Buffer } from 'node:buffer'
import { readFile, writeFile } from 'node:fs/promises' import { readFile, writeFile } from 'node:fs/promises'
import { createResolver } from '@nuxt/kit'
import { flatten, unflatten } from 'flat' import { flatten, unflatten } from 'flat'
import { currentLocales } from '../config/i18n' import { createResolver } from 'nuxt/kit'
import { currentLocales } from '../config/i18n.ts'
const resolver = createResolver(import.meta.url) const resolver = createResolver(import.meta.url)

View file

@ -1,4 +1,4 @@
import type { ThemeColors } from '~/composables/settings' import type { ThemeColors } from '../app/composables/settings'
import chroma from 'chroma-js' import chroma from 'chroma-js'
// #cc7d24 -> hcl(67.14,62.19,59.56) // #cc7d24 -> hcl(67.14,62.19,59.56)

View file

@ -1,13 +1,13 @@
import type { ElkTranslationStatus } from '#shared/types/translation-status'
import type { LocaleEntry } from '../docs/types' import type { LocaleEntry } from '../docs/types'
import type { ElkTranslationStatus } from '../shared/types/translation-status'
import { Buffer } from 'node:buffer' import { Buffer } from 'node:buffer'
import { readFile, writeFile } from 'node:fs/promises' import { readFile, writeFile } from 'node:fs/promises'
import { createResolver } from '@nuxt/kit'
import { flatten } from 'flat' import { flatten } from 'flat'
import { countryLocaleVariants, currentLocales } from '../config/i18n' import { createResolver } from 'nuxt/kit'
import { countryLocaleVariants, currentLocales } from '../config/i18n.ts'
export const localeData: [code: string, file: string[], title: string][] export const localeData: [code: string, file: string[], title: string][]
= currentLocales.map((l: any) => [l.code, l.files ? l.files : [l.file!], l.name ?? l.code]) = currentLocales.map((l: any) => [l.code, l.files ? l.files : [l.file!], l.name ?? l.code])
function merge(src: Record<string, any>, dst: Record<string, any>) { function merge(src: Record<string, any>, dst: Record<string, any>) {
for (const key in src) { for (const key in src) {

View file

@ -1,7 +1,7 @@
import process from 'node:process' import process from 'node:process'
import fs from 'fs-extra' import fs from 'fs-extra'
import { emojiPrefix, iconifyEmojiPackage } from '../config/emojis' import { emojiPrefix, iconifyEmojiPackage } from '../config/emojis.ts'
import { colorsMap } from './generate-themes' import { colorsMap } from './generate-themes.ts'
const dereference = process.platform === 'win32' ? true : undefined const dereference = process.platform === 'win32' ? true : undefined

View file

@ -1,6 +1,6 @@
import { stringifyQuery } from 'ufo' import { stringifyQuery } from 'ufo'
import { defaultUserAgent } from '~~/server/utils/shared' import { defaultUserAgent, invalidateApp } from '~~/server/utils/shared'
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
let { server, origin } = getRouterParams(event) let { server, origin } = getRouterParams(event)
@ -43,7 +43,51 @@ export default defineEventHandler(async (event) => {
const url = `/signin/callback?${stringifyQuery({ server, token: result.access_token, vapid_key: app.vapid_key })}` const url = `/signin/callback?${stringifyQuery({ server, token: result.access_token, vapid_key: app.vapid_key })}`
await sendRedirect(event, url, 302) await sendRedirect(event, url, 302)
} }
catch { catch (error: any) {
// Check for invalid client error (OAuth app deleted)
if (error?.data?.error === 'invalid_client'
|| (error?.statusCode === 401 && error?.data?.error_description?.includes('Client authentication failed'))) {
// Invalidate cached app and retry once
await invalidateApp(origin, server)
try {
const newApp = await getApp(origin, server)
if (!newApp) {
throw createError({
statusCode: 400,
statusMessage: `Failed to re-register app for server: ${server}`,
})
}
const retryResult: any = await $fetch(`https://${server}/oauth/token`, {
method: 'POST',
headers: {
'user-agent': defaultUserAgent,
},
body: {
client_id: newApp.client_id,
client_secret: newApp.client_secret,
redirect_uri: getRedirectURI(origin, server),
grant_type: 'authorization_code',
code,
scope: 'read write follow push',
},
retry: 1,
})
const url = `/signin/callback?${stringifyQuery({ server, token: retryResult.access_token, vapid_key: newApp.vapid_key })}`
await sendRedirect(event, url, 302)
return
}
catch {
throw createError({
statusCode: 400,
statusMessage: 'OAuth application recovery failed. Please try again.',
})
}
}
// Other errors (network, invalid code, etc.)
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'Could not complete log in.', statusMessage: 'Could not complete log in.',

View file

@ -111,6 +111,12 @@ export async function deleteApp(server: string) {
await storage.removeItem(key) await storage.removeItem(key)
} }
export async function invalidateApp(origin: string, server: string) {
const host = origin.replace(/^https?:\/\//, '').replace(/\W/g, '-').replace(/\?.*$/, '')
const key = `servers:v4:${server}:${host}.json`.toLowerCase()
await storage.removeItem(key)
}
export async function listServers() { export async function listServers() {
const keys = await storage.getKeys('servers:v4:') const keys = await storage.getKeys('servers:v4:')
const servers = new Set<string>() const servers = new Set<string>()

View file

@ -1,5 +1,6 @@
{ {
"extends": "../tsconfig.json", "extends": "../.nuxt/tsconfig.app.json",
"noEmit": true,
"compilerOptions": { "compilerOptions": {
"lib": ["ESNext", "WebWorker", "DOM.Iterable"], "lib": ["ESNext", "WebWorker", "DOM.Iterable"],
"types": ["vite/client"] "types": ["vite/client"]

View file

@ -0,0 +1 @@
{"root":["./elk-sw.ts","./notification.ts","./share-target.ts","./types.ts","./web-push-notifications.ts"],"version":"5.9.2"}

View file

@ -1,6 +1,6 @@
/// <reference lib="WebWorker" /> /// <reference lib="WebWorker" />
/// <reference types="vite/client" /> /// <reference types="vite/client" />
import type { PushPayload } from '~~/service-worker/types' import type { PushPayload } from './types'
import { ELK_PAGE_LIFECYCLE_FROZEN } from '../app/constants' import { ELK_PAGE_LIFECYCLE_FROZEN } from '../app/constants'
import { import {
closeDatabaseConnections, closeDatabaseConnections,

View file

@ -1,5 +1,5 @@
import type { RouteLocationRaw } from '#vue-router'
import type { mastodon } from 'masto' import type { mastodon } from 'masto'
import type { RouteLocationRaw } from 'vue-router'
import type { MarkNonNullable, Mutable } from './utils' import type { MarkNonNullable, Mutable } from './utils'
export interface AppInfo { export interface AppInfo {
@ -43,8 +43,6 @@ export interface GroupedLikeNotifications {
export type NotificationSlot = GroupedNotifications | GroupedLikeNotifications | mastodon.v1.Notification export type NotificationSlot = GroupedNotifications | GroupedLikeNotifications | mastodon.v1.Notification
export type TranslateFn = ReturnType<typeof useI18n>['t']
export interface DraftItem { export interface DraftItem {
editingStatus?: mastodon.v1.Status editingStatus?: mastodon.v1.Status
initialText?: string initialText?: string

View file

@ -13,7 +13,7 @@ function status(id: string, filtered?: mastodon.v1.FilterContext): mastodon.v1.S
if (filtered) { if (filtered) {
fakeStatus.filtered fakeStatus.filtered
= [ = [
{ {
filter: { filter: {
filterAction: 'hide', filterAction: 'hide',

View file

@ -1,3 +1,20 @@
{ {
"extends": "./.nuxt/tsconfig.json" "references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
},
{
"path": "./service-worker/tsconfig.json"
}
],
"files": []
} }

View file

@ -1,5 +1,3 @@
import type { Variant } from 'unocss'
import process from 'node:process'
import { variantParentMatcher } from '@unocss/preset-mini/utils' import { variantParentMatcher } from '@unocss/preset-mini/utils'
import { import {
@ -108,29 +106,14 @@ export default defineConfig({
}, },
}, },
variants: [ variants: [
...(process.env.TAURI_PLATFORM (matcher) => {
? <Variant<any>[]>[(matcher) => { if (!matcher.startsWith('native-mac:'))
if (!matcher.startsWith('native:')) return
return return {
return { matcher: matcher.slice(11),
matcher: matcher.slice(7), layer: 'native-mac',
layer: 'native', }
} },
}]
: []),
...(process.env.TAURI_PLATFORM !== 'macos'
? <Variant<any>[]>[
(matcher) => {
if (!matcher.startsWith('native-mac:'))
return
return {
matcher: matcher.slice(11),
layer: 'native-mac',
}
},
]
: []
),
variantParentMatcher('fullscreen', '@media (display-mode: fullscreen)'), variantParentMatcher('fullscreen', '@media (display-mode: fullscreen)'),
variantParentMatcher('coarse-pointer', '@media (pointer: coarse)'), variantParentMatcher('coarse-pointer', '@media (pointer: coarse)'),
], ],

View file

@ -1,22 +1,30 @@
import { defineVitestConfig } from '@nuxt/test-utils/config' import { defineVitestProject } from '@nuxt/test-utils/config'
import { isCI } from 'std-env' import { isCI } from 'std-env'
import { defineConfig } from 'vitest/config'
export default defineVitestConfig({ export default defineConfig({
define: { define: {
'process.test': 'true', 'process.test': 'true',
}, },
test: { test: {
reporters: isCI ? ['default', 'hanging-process'] : ['default'], reporters: isCI ? ['default', 'hanging-process'] : ['default'],
setupFiles: [ projects: [
'./tests/setup.ts', await defineVitestProject({
], test: {
environmentOptions: { name: 'nuxt',
nuxt: { setupFiles: [
mock: { './tests/setup.ts',
indexedDb: true, ],
intersectionObserver: true, environmentOptions: {
nuxt: {
mock: {
indexedDb: true,
intersectionObserver: true,
},
},
},
}, },
}, }),
}, ],
}, },
}) })