Implement translations
See also: https://github.com/kytta/share2fedi/pull/43 Co-authored-by: Sunny Ripert <sunny@sunfox.org>
This commit is contained in:
parent
31d95aa834
commit
89bb338065
10 changed files with 231 additions and 29 deletions
|
@ -13,14 +13,16 @@ const { prefilledText, prefilledInstance } = Astro.props;
|
||||||
action="/api/share"
|
action="/api/share"
|
||||||
method="POST"
|
method="POST"
|
||||||
>
|
>
|
||||||
<label>
|
<label data-translate="postText">
|
||||||
Post text
|
Post text
|
||||||
<textarea
|
<textarea
|
||||||
name="text"
|
name="text"
|
||||||
id="text"
|
id="text"
|
||||||
rows="7"
|
rows="7"
|
||||||
placeholder="What's on your mind?"
|
placeholder="What’s on your mind?"
|
||||||
required
|
required
|
||||||
|
data-translate="postTextPlaceholder"
|
||||||
|
data-translate-attribute="placeholder"
|
||||||
>{prefilledText}</textarea
|
>{prefilledText}</textarea
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
@ -30,5 +32,7 @@ const { prefilledText, prefilledInstance } = Astro.props;
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Publish"
|
value="Publish"
|
||||||
|
data-translate="publish"
|
||||||
|
data-translate-attribute="value"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -8,7 +8,7 @@ const { prefilledInstance } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<datalist id="popular-instances"></datalist>
|
<datalist id="popular-instances"></datalist>
|
||||||
<label>
|
<label data-translate="instance">
|
||||||
Fediverse instance
|
Fediverse instance
|
||||||
<div class="instance-input">
|
<div class="instance-input">
|
||||||
<span id="https-label">https://</span>
|
<span id="https-label">https://</span>
|
||||||
|
@ -16,7 +16,6 @@ const { prefilledInstance } = Astro.props;
|
||||||
type="text"
|
type="text"
|
||||||
name="instance"
|
name="instance"
|
||||||
id="instance"
|
id="instance"
|
||||||
placeholder="mastodon.social"
|
|
||||||
list="popular-instances"
|
list="popular-instances"
|
||||||
required
|
required
|
||||||
aria-describedby="https-label"
|
aria-describedby="https-label"
|
||||||
|
@ -25,15 +24,24 @@ const { prefilledInstance } = Astro.props;
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div id="saved-instances"></div>
|
<div
|
||||||
|
id="saved-instances"
|
||||||
|
data-translate="previouslyUsed"
|
||||||
|
>
|
||||||
|
Previously used:
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="remember">
|
<label
|
||||||
|
for="remember"
|
||||||
|
data-translate="rememberInstance"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="remember"
|
id="remember"
|
||||||
name="remember"
|
name="remember"
|
||||||
/>
|
/>
|
||||||
Remember my instance on this device
|
Remember instance on this device
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -61,6 +69,10 @@ const { prefilledInstance } = Astro.props;
|
||||||
#saved-instances {
|
#saved-instances {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
:global(span) {
|
:global(span) {
|
||||||
color: var(--s2f-accent-color-contrast);
|
color: var(--s2f-accent-color-contrast);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -87,8 +99,7 @@ const { prefilledInstance } = Astro.props;
|
||||||
instanceElement.value = getUrlDomain(savedInstances[0] as string);
|
instanceElement.value = getUrlDomain(savedInstances[0] as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector("#saved-instances")!.replaceChildren(
|
document.querySelector("#saved-instances>div")!.replaceChildren(
|
||||||
"Previously used: ",
|
|
||||||
...savedInstances
|
...savedInstances
|
||||||
.flatMap((instance: string) => {
|
.flatMap((instance: string) => {
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
|
|
55
src/components/language-select.astro
Normal file
55
src/components/language-select.astro
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
/*!
|
||||||
|
* © 2023 Nikita Karamov
|
||||||
|
* Licensed under AGPL v3 or later
|
||||||
|
*/
|
||||||
|
import { languages } from "@i18n/translations";
|
||||||
|
|
||||||
|
const initialLanguage = "en";
|
||||||
|
---
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span data-translate="language">Language:</span>
|
||||||
|
<select
|
||||||
|
name="language"
|
||||||
|
id="language"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
Object.entries(languages).map(([k, v]) => {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
selected={k === initialLanguage}
|
||||||
|
value={k}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { findBestLanguage } from "@i18n/engine";
|
||||||
|
import { applyTranslations } from "@i18n/engine";
|
||||||
|
import { $locale } from "@stores/i18n";
|
||||||
|
|
||||||
|
const select: HTMLSelectElement = document.querySelector("#language")!;
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
$locale.subscribe((newLocale) => {
|
||||||
|
if (newLocale === undefined) {
|
||||||
|
newLocale = findBestLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTranslations(newLocale);
|
||||||
|
if (select.value !== newLocale) {
|
||||||
|
select.value = newLocale;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
select.addEventListener("change", (event) => {
|
||||||
|
$locale.set((event.target as typeof select).value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
54
src/i18n/engine.ts
Normal file
54
src/i18n/engine.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { strings, defaultLanguage, languages } from "./translations";
|
||||||
|
|
||||||
|
export function useTranslations(language: string) {
|
||||||
|
if (!(language in strings)) {
|
||||||
|
language = defaultLanguage;
|
||||||
|
}
|
||||||
|
return function t(
|
||||||
|
key: keyof (typeof strings)[typeof defaultLanguage],
|
||||||
|
): string {
|
||||||
|
return strings[language]![key] || strings[defaultLanguage]![key] || "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findBestLanguage(): string {
|
||||||
|
let browserLanguages = navigator.languages;
|
||||||
|
if (!navigator.languages) browserLanguages = [navigator.language];
|
||||||
|
for (const language of browserLanguages) {
|
||||||
|
const locale = new Intl.Locale(language);
|
||||||
|
const minimized = locale.minimize();
|
||||||
|
|
||||||
|
for (const candidate of [locale.baseName, minimized.baseName]) {
|
||||||
|
if (candidate in languages) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTranslations(language: string) {
|
||||||
|
const t = useTranslations(language);
|
||||||
|
|
||||||
|
for (const node of document.querySelectorAll("[data-translate]")) {
|
||||||
|
const dataset = (node as HTMLElement).dataset;
|
||||||
|
|
||||||
|
if (dataset.translateAttribute) {
|
||||||
|
node.setAttribute(dataset.translateAttribute, t(dataset.translate!));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitTranslated = t(dataset.translate!).split("{}");
|
||||||
|
if (splitTranslated.length === 1) {
|
||||||
|
node.innerHTML = t(dataset.translate!);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of node.childNodes) {
|
||||||
|
if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
child.textContent = splitTranslated.shift() || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
src/i18n/translations.ts
Normal file
55
src/i18n/translations.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
export const languages = {
|
||||||
|
en: "English",
|
||||||
|
de: "Deutsch",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const strings: Record<keyof typeof languages, Record<string, string>> = {
|
||||||
|
en: {
|
||||||
|
title: "Share₂Fedi — an instance-agnostic share page for the Fediverse",
|
||||||
|
metaDescription:
|
||||||
|
"Share₂Fedi is a share page for Mastodon, Pleroma, Misskey, and others. Type in your post text and the instance URL and click ‘Publish!’",
|
||||||
|
language: "Language:",
|
||||||
|
description:
|
||||||
|
"Share₂Fedi is an instance-agnostic share page for {}. With it, you can post to various federated platforms from a single page.",
|
||||||
|
fediverse: "the Fediverse",
|
||||||
|
supportedProjects: "Supported projects:",
|
||||||
|
incl: "incl.",
|
||||||
|
credits:
|
||||||
|
"Share₂Fedi is developed and maintained by {}. Source code is {}. Hosted with {}. {}.",
|
||||||
|
onGitHub: "on GitHub",
|
||||||
|
statusPage: "Status page",
|
||||||
|
licence: "Licence",
|
||||||
|
privacyNotice: "Privacy Notice",
|
||||||
|
postText: "Post text{}",
|
||||||
|
postTextPlaceholder: "What’s on your mind?",
|
||||||
|
instance: "Fediverse instance{}",
|
||||||
|
previouslyUsed: "Previously used: {}",
|
||||||
|
rememberInstance: "{} Remember instance on this device",
|
||||||
|
publish: "Publish",
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
title: "Share₂Fedi — eine instanzunabhängige Share-Seite für das Fediverse",
|
||||||
|
metaDescription:
|
||||||
|
"Share₂Fedi ist eine Share-Seite für Mastodon, Pleroma, Misskey und andere. Geben Sie Ihren Beitragstext und die Instanz-URL ein und klicken Sie auf „Veröffentlichen“!",
|
||||||
|
language: "Sprache:",
|
||||||
|
description:
|
||||||
|
"Share₂Fedi ist eine instanzunabhängige Share-Seite für {}. Mit ihr können Sie von einer einzigen Seite aus auf verschiedenen föderierten Plattformen posten.",
|
||||||
|
fediverse: "das Fediverse",
|
||||||
|
supportedProjects: "Unterstützte Projekte:",
|
||||||
|
incl: "inkl.",
|
||||||
|
credits:
|
||||||
|
"Share₂Fedi wird von {} entwickelt und gepflegt. Der Quellcode ist {}. Gehostet mit {}. {}.",
|
||||||
|
onGitHub: "auf GitHub",
|
||||||
|
statusPage: "Statusseite",
|
||||||
|
licence: "Lizenz",
|
||||||
|
privacyNotice: "Datenschutzhinweis",
|
||||||
|
postText: "Beitragstext{}",
|
||||||
|
postTextPlaceholder: "Was gibt’s Neues?",
|
||||||
|
instance: "Fediverse-Instanz{}",
|
||||||
|
previouslyUsed: "Bisher verwendet: {}",
|
||||||
|
rememberInstance: "{} Instanz auf diesem Gerät merken",
|
||||||
|
publish: "Veröffentlichen",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const defaultLanguage: keyof typeof strings = "en";
|
|
@ -16,10 +16,12 @@ const { title } = Astro.props;
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0"
|
content="width=device-width, initial-scale=1.0"
|
||||||
/>
|
/>
|
||||||
<title>{title}</title>
|
<title data-translate="title">{title}</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Share₂Fedi is a share page for Mastodon, Pleroma, Misskey, and others. Type in your post text and the instance URL and click ‘Publish!’"
|
content="Share₂Fedi is a share page for Mastodon, Pleroma, Misskey, and others. Type in your post text and the instance URL and click ‘Publish!’"
|
||||||
|
data-translate="metaDescription"
|
||||||
|
data-translate-attribute="content"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="canonical"
|
rel="canonical"
|
||||||
|
|
|
@ -8,6 +8,8 @@ import Form from "@components/form.astro";
|
||||||
import { Content as PrivacyNotice } from "@pages/_privacy.md";
|
import { Content as PrivacyNotice } from "@pages/_privacy.md";
|
||||||
import { Content as Licence } from "@pages/_licence.md";
|
import { Content as Licence } from "@pages/_licence.md";
|
||||||
|
|
||||||
|
import LanguageSelect from "@components/language-select.astro";
|
||||||
|
|
||||||
const searchParameters = new URL(Astro.request.url).searchParams;
|
const searchParameters = new URL(Astro.request.url).searchParams;
|
||||||
const prefilledText = searchParameters.get("text");
|
const prefilledText = searchParameters.get("text");
|
||||||
const prefilledInstance = searchParameters.get("instance");
|
const prefilledInstance = searchParameters.get("instance");
|
||||||
|
@ -21,6 +23,8 @@ const prefilledInstance = searchParameters.get("instance");
|
||||||
width="195"
|
width="195"
|
||||||
height="60"
|
height="60"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LanguageSelect />
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<Form
|
<Form
|
||||||
|
@ -29,43 +33,53 @@ const prefilledInstance = searchParameters.get("instance");
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<aside>
|
<aside>
|
||||||
<p>
|
<p data-translate="description">
|
||||||
Share₂Fedi is an instance-agnostic share page for
|
Share₂Fedi is an instance-agnostic share page for
|
||||||
<a href="https://en.wikipedia.org/wiki/Fediverse">the Fediverse</a>. With
|
<a
|
||||||
it, you can post to various federated platforms from a single page.
|
href="https://en.wikipedia.org/wiki/Fediverse"
|
||||||
|
data-translate="fediverse"
|
||||||
|
>the Fediverse</a
|
||||||
|
>. With it, you can post to various federated platforms from a single
|
||||||
|
page.
|
||||||
</p>
|
</p>
|
||||||
<p><b>Supported projects:</b></p>
|
<p><b data-translate="supportedProjects">Supported projects:</b></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Mastodon (incl. Hometown, Fedibird, GlitchCafé)</li>
|
|
||||||
<li>
|
<li>
|
||||||
Pleroma (incl. Akkoma)
|
Mastodon (<span data-translate="incl">incl.</span> Hometown, Fedibird,
|
||||||
|
GlitchCafé)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Misskey (incl. Firefish/Calckey, FoundKey, Meisskey)
|
Pleroma (<span data-translate="incl">incl.</span> Akkoma)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Friendica
|
Misskey (<span data-translate="incl">incl.</span> Firefish/Calckey,
|
||||||
</li>
|
FoundKey, Meisskey)
|
||||||
<li>
|
|
||||||
Hubzilla
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
GNU Social
|
|
||||||
</li>
|
</li>
|
||||||
|
<li>Friendica</li>
|
||||||
|
<li>Hubzilla</li>
|
||||||
|
<li>GNU Social</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p data-translate="credits">
|
||||||
Share₂Fedi is developed and maintained by
|
Share₂Fedi is developed and maintained by
|
||||||
<a href="https://www.kytta.dev/">Nikita Karamov</a>. Source code is
|
<a href="https://www.kytta.dev/">Nikita Karamov</a>. Source code is
|
||||||
<a href="https://github.com/kytta/share2fedi">on GitHub</a>. Hosted with
|
<a
|
||||||
|
href="https://github.com/kytta/share2fedi"
|
||||||
|
data-translate="onGitHub"
|
||||||
|
>on GitHub</a
|
||||||
|
>. Hosted with
|
||||||
<a href="https://vercel.com">Vercel</a>.
|
<a href="https://vercel.com">Vercel</a>.
|
||||||
<a href="https://stats.uptimerobot.com/QOXj3uXPDX">Status page</a>.
|
<a
|
||||||
|
href="https://stats.uptimerobot.com/QOXj3uXPDX"
|
||||||
|
data-translate="statusPage"
|
||||||
|
>Status page</a
|
||||||
|
>.
|
||||||
</p>
|
</p>
|
||||||
<details>
|
<details>
|
||||||
<summary>Licence</summary>
|
<summary data-translate="licence">Licence</summary>
|
||||||
<Licence />
|
<Licence />
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>Privacy Notice</summary>
|
<summary data-translate="privacyNotice">Privacy Notice</summary>
|
||||||
<PrivacyNotice />
|
<PrivacyNotice />
|
||||||
</details>
|
</details>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
3
src/stores/i18n.ts
Normal file
3
src/stores/i18n.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { persistentAtom } from "@nanostores/persistent";
|
||||||
|
|
||||||
|
export const $locale = persistentAtom<string | undefined>("locale");
|
|
@ -47,6 +47,9 @@ body {
|
||||||
|
|
||||||
header {
|
header {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
main,
|
main,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@components/*": ["src/components/*"],
|
"@components/*": ["src/components/*"],
|
||||||
|
"@i18n/*": ["src/i18n/*"],
|
||||||
"@layouts/*": ["src/layouts/*"],
|
"@layouts/*": ["src/layouts/*"],
|
||||||
"@pages/*": ["src/pages/*"],
|
"@pages/*": ["src/pages/*"],
|
||||||
"@scripts/*": ["src/scripts/*"],
|
"@scripts/*": ["src/scripts/*"],
|
||||||
|
|
Reference in a new issue