diff --git a/.env.example.en b/.env.example.en
new file mode 100644
index 0000000..a939838
--- /dev/null
+++ b/.env.example.en
@@ -0,0 +1,18 @@
+# Example .env file for local development
+# For production, environment variables are set on the production server
+
+# SvelteKit settings
+ORIGIN=http://localhost:5173
+
+# PostOwl settings
+DB_PATH="./data/db.sqlite3"
+ADMIN_PASSWORD="your-secret-password"
+ADMIN_NAME="Your Name"
+ADMIN_EMAIL="you@your-domain.com"
+INITIAL_MESSAGE="Welcome to my PostOwl website!"
+
+# SMTP settings for dev if using mailpit - see README for instructions
+SMTP_SERVER="localhost"
+SMTP_PORT="1025"
+SMTP_USERNAME="postmaster@localhost" # can be whatever you want in dev
+SMTP_PASSWORD="password" # can be whatever you want in dev
diff --git a/.env.example.pl b/.env.example.pl
new file mode 100644
index 0000000..3a67e94
--- /dev/null
+++ b/.env.example.pl
@@ -0,0 +1,18 @@
+# Example .env file for local development
+# For production, environment variables are set on the production server
+
+# SvelteKit settings
+ORIGIN=http://localhost:5173
+
+# PostOwl settings
+DB_PATH="./data/db.sqlite3"
+ADMIN_PASSWORD="your-secret-password"
+ADMIN_NAME="Twoja Nazwa"
+ADMIN_EMAIL="you@your-domain.com"
+INITIAL_MESSAGE="Witamy na mojej stronie PostOwl!"
+
+# SMTP settings for dev if using mailpit - see README for instructions
+SMTP_SERVER="localhost"
+SMTP_PORT="1025"
+SMTP_USERNAME="postmaster@localhost" # can be whatever you want in dev
+SMTP_PASSWORD="password" # can be whatever you want in dev
diff --git a/.gitignore b/.gitignore
index 8b714f2..5ef8046 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
.DS_Store
.env*
!.env.example
+!.env.example.*
!.env.production.example
*.sqlite3*
.tool-versions
diff --git a/Dockerfile.dev b/Dockerfile.dev
new file mode 100644
index 0000000..7cda711
--- /dev/null
+++ b/Dockerfile.dev
@@ -0,0 +1,54 @@
+# syntax = docker/dockerfile:1
+
+ARG NODE_VERSION=20.11.1
+FROM node:${NODE_VERSION}-slim AS base
+
+LABEL fly_launch_runtime="Node.js"
+
+# Node.js app lives here
+WORKDIR /app
+
+# Set production environment
+ENV NODE_ENV="production"
+
+# Throw-away build stage to reduce size of final image
+FROM base AS build
+
+# Install packages needed to build node modules
+RUN apt-get update -qq && \
+ apt-get install -y build-essential pkg-config python-is-python3
+
+# Install node modules
+COPY --link .npmrc package-lock.json package.json ./
+RUN npm ci --include=dev
+
+# Copy application code
+COPY --link . .
+
+# Build application
+RUN mkdir /data && npm run build
+
+# Remove development dependencies
+RUN npm prune --omit=dev
+
+# Final stage for app image
+FROM base
+
+# Install packages needed for deployment
+RUN apt-get update -qq && \
+ apt-get install --no-install-recommends -y sqlite3 && \
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
+
+# Copy built application
+COPY --from=build /app /app
+
+# Setup sqlite3 on a separate volume
+RUN mkdir -p /data
+VOLUME /data
+RUN chmod +x /app/scripts/create_sqlite.sh
+RUN /app/scripts/create_sqlite.sh
+
+# Start the server by default, this can be overwritten at runtime
+EXPOSE 3000
+ENV DATABASE_URL="file:///data/sqlite.db"
+CMD [ "npm", "run", "start" ]
diff --git a/package-lock.json b/package-lock.json
index 9e4ea20..5b1dcfb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,8 @@
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.5.3",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
+ "@sveltekit-i18n/base": "^1.3.7",
+ "@sveltekit-i18n/parser-icu": "^1.0.8",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
@@ -497,6 +499,60 @@
"node": ">=16.0.0"
}
},
+ "node_modules/@formatjs/ecma402-abstract": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
+ "integrity": "sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.5.4",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz",
+ "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser": {
+ "version": "2.7.8",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.8.tgz",
+ "integrity": "sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.0.0",
+ "@formatjs/icu-skeleton-parser": "1.8.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.2.tgz",
+ "integrity": "sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.0.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
+ "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -1046,6 +1102,26 @@
"vite": "^5.0.0"
}
},
+ "node_modules/@sveltekit-i18n/base": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/@sveltekit-i18n/base/-/base-1.3.7.tgz",
+ "integrity": "sha512-kg1kql1/ro/lIudwFiWrv949Q07gmweln87tflUZR51MNdXXzK4fiJQv5Mw50K/CdQ5BOk/dJ0WOH2vOtBI6yw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "svelte": ">=3.49.0"
+ }
+ },
+ "node_modules/@sveltekit-i18n/parser-icu": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@sveltekit-i18n/parser-icu/-/parser-icu-1.0.8.tgz",
+ "integrity": "sha512-/LnvE1EJv+higIxB5cWIV+9neiOe+CfC7VKhpv9mnU35NcZO3yOhEZ8y6F8nHHkMYIABLcqr15yk4hSvmRGWDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "intl-messageformat": "^10.1.1"
+ }
+ },
"node_modules/@tailwindcss/forms": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
@@ -2435,6 +2511,19 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
+ "node_modules/intl-messageformat": {
+ "version": "10.5.14",
+ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.14.tgz",
+ "integrity": "sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.0.0",
+ "@formatjs/fast-memoize": "2.2.0",
+ "@formatjs/icu-messageformat-parser": "2.7.8",
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -4304,6 +4393,13 @@
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
},
+ "node_modules/tslib": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
+ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
+ "dev": true,
+ "license": "0BSD"
+ },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
diff --git a/package.json b/package.json
index 48f5d7b..ffef987 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,8 @@
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.5.3",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
+ "@sveltekit-i18n/base": "^1.3.7",
+ "@sveltekit-i18n/parser-icu": "^1.0.8",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
diff --git a/scripts/create_sqlite.sh b/scripts/create_sqlite.sh
new file mode 100755
index 0000000..3f979be
--- /dev/null
+++ b/scripts/create_sqlite.sh
@@ -0,0 +1,5 @@
+FILE="./data/db.sqlite3"
+if ! [ -f "$FILE" ]
+then
+ sqlite3 $FILE < ./scripts/schema.sql
+fi
diff --git a/src/hooks.server.js b/src/hooks.server.js
index e6bccd3..cd07af0 100644
--- a/src/hooks.server.js
+++ b/src/hooks.server.js
@@ -4,6 +4,7 @@ export async function handle({ event, resolve }) {
// ATTENTION: Never expose anything to event.locals that shouldn't be seen by the client
// We mix in ...local to data objects on server routes
event.locals.currentUser = await getCurrentUser(event.cookies.get('sessionid'));
+ console.log(`src/hooks.server.js: ${event.locals.currentUser?.name}`);
event.locals.bio = await getBio();
event.locals.counts = await getCounts();
event.locals.origin = event.url.origin;
diff --git a/src/lib/components/EditorControls.svelte b/src/lib/components/EditorControls.svelte
index c1a35e3..19d61c2 100644
--- a/src/lib/components/EditorControls.svelte
+++ b/src/lib/components/EditorControls.svelte
@@ -1,184 +1,186 @@
-
-
-
-
- {#if editorState}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/if}
+
+
+
+
+ {#if editorState}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/if}
-
-
Cancel
-
-
{confirmLabel}
-
-
-
+
+
{$t('common.cancel')}
+
+
{confirmLabel}
+
+
+
diff --git a/src/lib/components/EditorToolbar.svelte b/src/lib/components/EditorToolbar.svelte
index baf025a..47320cd 100644
--- a/src/lib/components/EditorToolbar.svelte
+++ b/src/lib/components/EditorToolbar.svelte
@@ -1,8 +1,10 @@
{#await import('$lib/components/EditorControls.svelte') then EditorToolbar}
-
+
{/await}
diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte
index 03a039e..e0ffb75 100644
--- a/src/lib/components/Footer.svelte
+++ b/src/lib/components/Footer.svelte
@@ -1,27 +1,31 @@
-
-
-
- Powered by
PostOwl. Page
- viewed {count || '…'} times.
-
-
-
+
+
+
+ {@html $t('common.poweredBy', {
+ value: '
PostOwl'
+ })}
+ {count ? $t('common.pageView', { value: count }) : $t('common.pageNotViewed')}
+
+
+
diff --git a/src/lib/components/ImageEditor.svelte b/src/lib/components/ImageEditor.svelte
index 8a38a27..3182af9 100644
--- a/src/lib/components/ImageEditor.svelte
+++ b/src/lib/components/ImageEditor.svelte
@@ -1,172 +1,173 @@
- {#if is_safari()}
- ATTENTION: Use Google Chrome, Firefox, oder Microsoft Edge for
- optimized image quality and size.
- {:else}
- Confirm with ENTER. Cancel with ESC.
- {/if}
- {#if progress}
- {progress} uploading...
- {/if}
+ {#if is_safari()}
+ {$t('common.ATTENTION')}: {$t('common.notUseSafari')}
+ {:else}
+ {$t('common.confirmWithEnter')}.
+ {/if}
+ {#if progress}
+ {progress} uploading...
+ {/if}
{#if is_cropping}
-
+
+
+
+
+
+
{/if}
- {#if is_cropping}
-
(cropDetail = e.detail)}
- aspect={maxWidth / maxHeight}
- />
- {:else}
-
- fileInput.click()}
- class={className +
- ' cursor-pointer outline-[2px] hover:outline-dashed outline-[#EF174C] -outline-offset-[2px]'}
- {src}
- {alt}
- title={uploadPrompt}
- />
- {/if}
+ {#if is_cropping}
+ (cropDetail = e.detail)}
+ aspect={maxWidth / maxHeight}
+ />
+ {:else}
+
+ fileInput.click()}
+ class={className +
+ ' cursor-pointer outline-[2px] hover:outline-dashed outline-[#EF174C] -outline-offset-[2px]'}
+ {src}
+ {alt}
+ title={uploadPrompt}
+ />
+ {/if}
diff --git a/src/lib/components/PlainTextEditor.svelte b/src/lib/components/PlainTextEditor.svelte
index 2072a9d..f8f7f3c 100644
--- a/src/lib/components/PlainTextEditor.svelte
+++ b/src/lib/components/PlainTextEditor.svelte
@@ -1,91 +1,92 @@
diff --git a/src/lib/components/Post.svelte b/src/lib/components/Post.svelte
index 1fd0753..5490001 100644
--- a/src/lib/components/Post.svelte
+++ b/src/lib/components/Post.svelte
@@ -1,37 +1,45 @@
-
-
- {formatDate(created_at)}
-
-
-
-
-
+
+
+
+ {$t(
+ 'common.date',
+ { value: new Date(created_at) },
+ { date: { FULL: { dateStyle: 'medium' } } }
+ )}
+
+
+
+
+
+
diff --git a/src/lib/components/PostTeaser.svelte b/src/lib/components/PostTeaser.svelte
index 5a234eb..0f7b5d4 100644
--- a/src/lib/components/PostTeaser.svelte
+++ b/src/lib/components/PostTeaser.svelte
@@ -1,80 +1,91 @@
-
- {#if currentUser}
-
-
-
- {#if post.is_public}
- Public
- {#if post.recipients.length > 0}
- (and sent to {lf.format(post.recipients.map(r => r.name))}){/if}
- {:else if post.recipients.length > 0}
- Shared (sent to {lf.format(post.recipients.map(r => r.name))})
- {:else}
- Private
- {/if}
-
- {#if post.is_public}
-
- {/if}
-
- {/if}
-
- {#if teaser_image?.src && teaser_image?.width && teaser_image?.height}
-
-
-
- {/if}
-
-
-
- Continue reading →
-
+
+ {#if currentUser}
+
+
+
+ {#if post.is_public}
+ {$t('common.public', { gender: 'male' })}
+ {#if post.recipients.length > 0}
+ (and sent to {lf.format(post.recipients.map(r => r.name))}){/if}
+ {:else if post.recipients.length > 0}
+ {$t('common.shared')} (sent to {lf.format(
+ post.recipients.map(r => r.name)
+ )})
+ {:else}
+ {$t('common.private', { gender: 'male' })}
+ {/if}
+
+ {#if post.is_public}
+
+ {/if}
+
+ {/if}
+
+ {#if teaser_image?.src && teaser_image?.width && teaser_image?.height}
+
+
+
+ {/if}
+
+
+
+ {$t('common.continueRead')} →
+
diff --git a/src/lib/components/RecipientsSelector.svelte b/src/lib/components/RecipientsSelector.svelte
index 91f2c24..f8fe027 100644
--- a/src/lib/components/RecipientsSelector.svelte
+++ b/src/lib/components/RecipientsSelector.svelte
@@ -3,6 +3,8 @@
import { debounce, classNames, isEmailValid } from '$lib/util';
import CopyableRecipient from './CopyableRecipient.svelte';
+ import { t } from '$lib/translations';
+
export let recipients = [];
export let is_public;
export let editable;
@@ -120,7 +122,11 @@
chooseVisibility ? 'z-50' : '' // pop to the top while editing visibility
)}
>
- {is_public ? 'Public' : 'Private'}
+ {is_public
+ ? $t('common.public', { gender: 'male' })
+ : $t('common.private', { gender: 'male' })}
+
{#if editable}