mirror of
https://github.com/svemagie/obsidian-micropub.git
synced 2026-05-14 19:38:50 +02:00
fix: gitignore docs/superpowers
This commit is contained in:
@@ -1,526 +0,0 @@
|
||||
# i18n (en + de) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add English and German translations to obsidian-micropub by extracting all hardcoded UI strings into locale files and wiring them through a tiny `t()` helper.
|
||||
|
||||
**Architecture:** A `src/i18n.ts` module exports a single `t(key, vars?)` function that reads `window.moment.locale()` (Obsidian's own locale) at call time, looks up the key in the matching locale map (falling back to `en`), and does `{var}` substitution. Locale maps live in `src/lang/en.ts` and `src/lang/de.ts`. No external dependencies, no build changes.
|
||||
|
||||
**Tech Stack:** TypeScript, Obsidian API (`window.moment.locale()`), esbuild (existing)
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| Action | Path | Responsibility |
|
||||
|--------|------|---------------|
|
||||
| Create | `src/lang/en.ts` | English string map (source of truth) |
|
||||
| Create | `src/lang/de.ts` | German string map |
|
||||
| Create | `src/i18n.ts` | `t()` helper — locale detection + lookup + interpolation |
|
||||
| Modify | `src/main.ts` | Replace hardcoded strings with `t()` calls |
|
||||
| Modify | `src/SettingsTab.ts` | Replace hardcoded strings with `t()` calls |
|
||||
| Modify | `src/SyndicationDialog.ts` | Replace hardcoded strings with `t()` calls |
|
||||
| Modify | `src/IndieAuth.ts` | Replace one hardcoded error string with `t()` call |
|
||||
| Modify | `README.md` | Note i18n support under Configuration |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: English locale file
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lang/en.ts`
|
||||
|
||||
- [ ] **Step 1: Create `src/lang/en.ts` with all UI strings**
|
||||
|
||||
```typescript
|
||||
// src/lang/en.ts
|
||||
export const en: Record<string, string> = {
|
||||
// Commands & ribbon
|
||||
cmdPublish: "Publish to Micropub",
|
||||
cmdUpdate: "Update existing Micropub post",
|
||||
|
||||
// Notices — main.ts
|
||||
noticeOpenNote: "Open a Markdown note to publish.",
|
||||
noticeNoEndpoint: "⚠️ Micropub endpoint not configured. Open plugin settings to add it.",
|
||||
noticeNoToken: "⚠️ Access token not configured. Open plugin settings to add it.",
|
||||
noticePublishing: "Publishing…",
|
||||
noticePublished: "✅ Published!",
|
||||
noticePublishFailed: "❌ Publish failed: {error}",
|
||||
noticeError: "❌ Error: {error}",
|
||||
noticeNoSyndTargets: "⚠️ Could not fetch syndication targets. Publishing without dialog.",
|
||||
|
||||
// Settings headings
|
||||
settingsTitle: "Micropub Publisher",
|
||||
settingsAccount: "Account",
|
||||
settingsEndpoints: "Endpoints",
|
||||
settingsEndpointsHint: "These are filled automatically when you sign in. Only edit them manually if your server uses non-standard paths.",
|
||||
settingsPublishBehaviour:"Publish Behaviour",
|
||||
settingsDigitalGarden: "Digital Garden",
|
||||
|
||||
// Settings — endpoints
|
||||
settingMicropubEndpoint: "Micropub endpoint",
|
||||
settingMicropubEndpointDesc: "e.g. https://example.com/micropub", // intentional: replaces personal domain in source
|
||||
settingMediaEndpoint: "Media endpoint",
|
||||
settingMediaEndpointDesc:"For image uploads. Auto-discovered if blank.",
|
||||
|
||||
// Settings — publish behaviour
|
||||
settingVisibility: "Default visibility",
|
||||
settingVisibilityDesc: "Applies when the note has no explicit visibility property.",
|
||||
visibilityPublic: "Public",
|
||||
visibilityUnlisted: "Unlisted",
|
||||
visibilityPrivate: "Private",
|
||||
|
||||
settingWriteUrl: "Write URL back to note",
|
||||
settingWriteUrlDesc: "After publishing, store the post URL as `mp-url` in frontmatter. Subsequent publishes will update the existing post instead of creating a new one.",
|
||||
|
||||
settingSyndDialog: "Syndication dialog",
|
||||
settingSyndDialogDesc: "When to show the cross-posting dialog before publishing. 'When needed' shows it only if the note has no mp-syndicate-to frontmatter.",
|
||||
syndDialogWhenNeeded: "When needed",
|
||||
syndDialogAlways: "Always",
|
||||
syndDialogNever: "Never",
|
||||
|
||||
settingSyndDefaults: "Default syndication targets",
|
||||
settingSyndDefaultsNone: "None configured. Targets checked by default in the publish dialog.",
|
||||
btnClearDefaults: "Clear defaults",
|
||||
|
||||
// Settings — digital garden
|
||||
settingGardenTags: "Map #garden/* tags to gardenStage",
|
||||
settingGardenTagsDesc: "Obsidian tags like #garden/plant become a `garden-stage: plant` Micropub property. The blog renders these as growth stage badges at /garden/.",
|
||||
settingGardenStages: "Stages: plant 🌱 · cultivate 🌿 · question ❓ · repot 🪴 · revitalize ✨ · revisit 🔄",
|
||||
|
||||
// Settings — sign-in / sign-out
|
||||
settingSiteUrl: "Site URL",
|
||||
settingSiteUrlDesc: "Your site's home page. Clicking Sign in opens your blog's login page in the browser — the same flow iA Writer uses.",
|
||||
settingSiteUrlPlaceholder: "https://example.com", // intentional: replaces personal domain in source
|
||||
btnSignIn: "Sign in",
|
||||
btnOpeningBrowser: "Opening browser…",
|
||||
noticeEnterSiteUrl: "Enter your site URL first.",
|
||||
noticeSignedInAs: "✅ Signed in as {me}",
|
||||
noticeSignInFailed: "Sign-in failed: {error}",
|
||||
lblSignedIn: "Signed in",
|
||||
btnSignOut: "Sign out",
|
||||
manualTokenSummary: "Or paste a token manually",
|
||||
settingAccessToken: "Access token",
|
||||
settingAccessTokenDesc: "Bearer token from your Indiekit admin panel.",
|
||||
btnVerify: "Verify",
|
||||
noticeSetEndpointFirst: "Set the Micropub endpoint and token first.",
|
||||
noticeTokenValid: "✅ Token is valid!",
|
||||
noticeTokenCheckFailed: "Token check failed: {error}",
|
||||
|
||||
// Syndication dialog
|
||||
syndDialogTitle: "Syndication targets",
|
||||
syndDialogSubtitle: "Choose where to cross-post this note.",
|
||||
btnCancel: "Cancel",
|
||||
btnPublish: "Publish",
|
||||
|
||||
// IndieAuth
|
||||
errSignInTimeout: "Sign-in timed out (5 min). Please try again.",
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build to confirm no TypeScript errors**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
|
||||
```
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
|
||||
git add src/lang/en.ts
|
||||
git commit -m "feat(i18n): add English locale file with all UI strings"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: German locale file
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lang/de.ts`
|
||||
|
||||
- [ ] **Step 1: Create `src/lang/de.ts`**
|
||||
|
||||
```typescript
|
||||
// src/lang/de.ts
|
||||
export const de: Record<string, string> = {
|
||||
// Commands & ribbon
|
||||
cmdPublish: "An Micropub veröffentlichen",
|
||||
cmdUpdate: "Bestehenden Micropub-Beitrag aktualisieren",
|
||||
|
||||
// Notices — main.ts
|
||||
noticeOpenNote: "Öffne eine Markdown-Notiz zum Veröffentlichen.",
|
||||
noticeNoEndpoint: "⚠️ Micropub-Endpunkt nicht konfiguriert. Bitte in den Plugin-Einstellungen eintragen.",
|
||||
noticeNoToken: "⚠️ Zugriffstoken nicht konfiguriert. Bitte in den Plugin-Einstellungen eintragen.",
|
||||
noticePublishing: "Wird veröffentlicht…",
|
||||
noticePublished: "✅ Veröffentlicht!",
|
||||
noticePublishFailed: "❌ Veröffentlichung fehlgeschlagen: {error}",
|
||||
noticeError: "❌ Fehler: {error}",
|
||||
noticeNoSyndTargets: "⚠️ Syndizierungsziele konnten nicht abgerufen werden. Veröffentlichung ohne Dialog.",
|
||||
|
||||
// Settings headings
|
||||
settingsTitle: "Micropub Publisher",
|
||||
settingsAccount: "Konto",
|
||||
settingsEndpoints: "Endpunkte",
|
||||
settingsEndpointsHint: "Diese werden beim Anmelden automatisch ausgefüllt. Nur manuell bearbeiten, wenn der Server nicht standardmäßige Pfade verwendet.",
|
||||
settingsPublishBehaviour:"Veröffentlichungsverhalten",
|
||||
settingsDigitalGarden: "Digitaler Garten",
|
||||
|
||||
// Settings — endpoints
|
||||
settingMicropubEndpoint: "Micropub-Endpunkt",
|
||||
settingMicropubEndpointDesc: "z. B. https://example.com/micropub",
|
||||
settingMediaEndpoint: "Medien-Endpunkt",
|
||||
settingMediaEndpointDesc:"Für Bild-Uploads. Wird automatisch ermittelt, wenn leer.",
|
||||
|
||||
// Settings — publish behaviour
|
||||
settingVisibility: "Standard-Sichtbarkeit",
|
||||
settingVisibilityDesc: "Gilt, wenn die Notiz keine explizite Sichtbarkeits-Eigenschaft hat.",
|
||||
visibilityPublic: "Öffentlich",
|
||||
visibilityUnlisted: "Nicht gelistet",
|
||||
visibilityPrivate: "Privat",
|
||||
|
||||
settingWriteUrl: "URL zurück in Notiz schreiben",
|
||||
settingWriteUrlDesc: "Nach der Veröffentlichung wird die Beitrags-URL als `mp-url` im Frontmatter gespeichert. Spätere Veröffentlichungen aktualisieren den Beitrag statt einen neuen zu erstellen.",
|
||||
|
||||
settingSyndDialog: "Syndizierungsdialog",
|
||||
settingSyndDialogDesc: "Wann der Dialog zum Querverweis vor der Veröffentlichung angezeigt wird. 'Bei Bedarf' zeigt ihn nur, wenn kein mp-syndicate-to im Frontmatter vorhanden ist.",
|
||||
syndDialogWhenNeeded: "Bei Bedarf",
|
||||
syndDialogAlways: "Immer",
|
||||
syndDialogNever: "Nie",
|
||||
|
||||
settingSyndDefaults: "Standard-Syndizierungsziele",
|
||||
settingSyndDefaultsNone: "Keine konfiguriert. Im Veröffentlichungsdialog standardmäßig aktivierte Ziele.",
|
||||
btnClearDefaults: "Standards löschen",
|
||||
|
||||
// Settings — digital garden
|
||||
settingGardenTags: "#garden/*-Tags zu gardenStage zuordnen",
|
||||
settingGardenTagsDesc: "Obsidian-Tags wie #garden/plant werden zur Micropub-Eigenschaft `garden-stage: plant`. Der Blog zeigt diese als Wachstumsstufen-Abzeichen unter /garden/ an.",
|
||||
settingGardenStages: "Stufen: plant 🌱 · cultivate 🌿 · question ❓ · repot 🪴 · revitalize ✨ · revisit 🔄",
|
||||
|
||||
// Settings — sign-in / sign-out
|
||||
settingSiteUrl: "Website-URL",
|
||||
settingSiteUrlDesc: "Startseite deiner Website. Klick auf Anmelden öffnet die Login-Seite deines Blogs im Browser.",
|
||||
settingSiteUrlPlaceholder: "https://example.com",
|
||||
btnSignIn: "Anmelden",
|
||||
btnOpeningBrowser: "Browser wird geöffnet…",
|
||||
noticeEnterSiteUrl: "Bitte zuerst die Website-URL eingeben.",
|
||||
noticeSignedInAs: "✅ Angemeldet als {me}",
|
||||
noticeSignInFailed: "Anmeldung fehlgeschlagen: {error}",
|
||||
lblSignedIn: "Angemeldet",
|
||||
btnSignOut: "Abmelden",
|
||||
manualTokenSummary: "Oder Token manuell einfügen",
|
||||
settingAccessToken: "Zugriffstoken",
|
||||
settingAccessTokenDesc: "Bearer-Token aus deinem Indiekit-Adminbereich.",
|
||||
btnVerify: "Prüfen",
|
||||
noticeSetEndpointFirst: "Bitte zuerst Micropub-Endpunkt und Token eingeben.",
|
||||
noticeTokenValid: "✅ Token ist gültig!",
|
||||
noticeTokenCheckFailed: "Token-Prüfung fehlgeschlagen: {error}",
|
||||
|
||||
// Syndication dialog
|
||||
syndDialogTitle: "Syndizierungsziele",
|
||||
syndDialogSubtitle: "Wo soll diese Notiz gleichzeitig veröffentlicht werden?",
|
||||
btnCancel: "Abbrechen",
|
||||
btnPublish: "Veröffentlichen",
|
||||
|
||||
// IndieAuth
|
||||
errSignInTimeout: "Anmeldung abgelaufen (5 Min.). Bitte erneut versuchen.",
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
|
||||
```
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
|
||||
git add src/lang/de.ts
|
||||
git commit -m "feat(i18n): add German locale file"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: i18n helper
|
||||
|
||||
**Files:**
|
||||
- Create: `src/i18n.ts`
|
||||
|
||||
- [ ] **Step 1: Create `src/i18n.ts`**
|
||||
|
||||
```typescript
|
||||
// src/i18n.ts
|
||||
import { en } from "./lang/en";
|
||||
import { de } from "./lang/de";
|
||||
|
||||
const locales: Record<string, Record<string, string>> = { en, de };
|
||||
|
||||
/**
|
||||
* Returns the translated string for `key` in the active Obsidian locale.
|
||||
* Falls back to English if the locale or key is missing.
|
||||
*
|
||||
* Supports `{var}` interpolation:
|
||||
* t("noticePublishFailed", { error: "500" })
|
||||
* → "❌ Publish failed: 500"
|
||||
*/
|
||||
export function t(key: string, vars?: Record<string, string>): string {
|
||||
const lang = (window.moment?.locale() ?? "en").split("-")[0];
|
||||
const map = locales[lang] ?? locales["en"];
|
||||
let str = map[key] ?? locales["en"][key] ?? key;
|
||||
|
||||
if (vars) {
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
str = str.split(`{${k}}`).join(v); // replaceAll not available in ES2018
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
|
||||
```
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
|
||||
git add src/i18n.ts
|
||||
git commit -m "feat(i18n): add t() helper with locale detection and interpolation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Wire `t()` into `main.ts`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/main.ts`
|
||||
|
||||
- [ ] **Step 1: Add import and replace all strings**
|
||||
|
||||
At the top of `main.ts`, add:
|
||||
```typescript
|
||||
import { t } from "./i18n";
|
||||
```
|
||||
|
||||
Then replace each hardcoded string:
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| `"Publish to Micropub"` (command name, line 36) | `t("cmdPublish")` |
|
||||
| `"Update existing Micropub post"` (line 49) | `t("cmdUpdate")` |
|
||||
| `"Publish to Micropub"` (ribbon tooltip, line 75) | `t("cmdPublish")` |
|
||||
| `"Open a Markdown note to publish."` (line 78) | `t("noticeOpenNote")` |
|
||||
| `"⚠️ Micropub endpoint not configured…"` (line 93) | `t("noticeNoEndpoint")` |
|
||||
| `"⚠️ Access token not configured…"` (line 100) | `t("noticeNoToken")` |
|
||||
| `"Publishing…"` (line 114) | `t("noticePublishing")` |
|
||||
| `` `✅ Published!${urlDisplay}` `` (line 126) | `` `${t("noticePublished")}${urlDisplay}` `` |
|
||||
| `` `❌ Publish failed: ${result.error}` `` (line 128) | `t("noticePublishFailed", { error: result.error ?? "" })` |
|
||||
| `` `❌ Error: ${msg}` `` (line 134) | `t("noticeError", { error: msg })` |
|
||||
| `"⚠️ Could not fetch syndication targets…"` (line 167) | `t("noticeNoSyndTargets")` |
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
|
||||
```
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
|
||||
git add src/main.ts
|
||||
git commit -m "feat(i18n): wire t() into main.ts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Wire `t()` into `SettingsTab.ts`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/SettingsTab.ts`
|
||||
|
||||
- [ ] **Step 1: Add import**
|
||||
|
||||
```typescript
|
||||
import { t } from "./i18n";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace strings in `display()`**
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| `"Micropub Publisher"` (h2) | `t("settingsTitle")` |
|
||||
| `"Account"` (h3) | `t("settingsAccount")` |
|
||||
| `"Endpoints"` (h3) | `t("settingsEndpoints")` |
|
||||
| `"These are filled automatically…"` (p) | `t("settingsEndpointsHint")` |
|
||||
| `"Micropub endpoint"` (setName) | `t("settingMicropubEndpoint")` |
|
||||
| `"e.g. https://blog.giersig.eu/micropub"` (setDesc) | `t("settingMicropubEndpointDesc")` |
|
||||
| `"Media endpoint"` (setName) | `t("settingMediaEndpoint")` |
|
||||
| `"For image uploads…"` (setDesc) | `t("settingMediaEndpointDesc")` |
|
||||
| `"Publish Behaviour"` (h3) | `t("settingsPublishBehaviour")` |
|
||||
| `"Default visibility"` (setName) | `t("settingVisibility")` |
|
||||
| `"Applies when the note…"` (setDesc) | `t("settingVisibilityDesc")` |
|
||||
| `"Public"` | `t("visibilityPublic")` |
|
||||
| `"Unlisted"` | `t("visibilityUnlisted")` |
|
||||
| `"Private"` | `t("visibilityPrivate")` |
|
||||
| `"Write URL back to note"` (setName) | `t("settingWriteUrl")` |
|
||||
| `"After publishing, store…"` (setDesc) | `t("settingWriteUrlDesc")` |
|
||||
| `"Syndication dialog"` (setName) | `t("settingSyndDialog")` |
|
||||
| `"When to show the cross-posting…"` (setDesc) | `t("settingSyndDialogDesc")` |
|
||||
| `"When needed"` | `t("syndDialogWhenNeeded")` |
|
||||
| `"Always"` | `t("syndDialogAlways")` |
|
||||
| `"Never"` | `t("syndDialogNever")` |
|
||||
| `"Default syndication targets"` (setName) | `t("settingSyndDefaults")` |
|
||||
| `"None configured…"` (false branch of setDesc — when `defaults.length === 0`) | `t("settingSyndDefaultsNone")` |
|
||||
| `"Clear defaults"` (btn) | `t("btnClearDefaults")` |
|
||||
| `"Digital Garden"` (h3) | `t("settingsDigitalGarden")` |
|
||||
| `"Map #garden/* tags…"` (setName) | `t("settingGardenTags")` |
|
||||
| `"Obsidian tags like…"` (setDesc) | `t("settingGardenTagsDesc")` |
|
||||
| `"Stages: plant 🌱…"` (p) | `t("settingGardenStages")` |
|
||||
|
||||
- [ ] **Step 3: Replace strings in `renderSignedOut()`**
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| `"Site URL"` (setName) | `t("settingSiteUrl")` |
|
||||
| `"Your site's home page…"` (setDesc) | `t("settingSiteUrlDesc")` |
|
||||
| `"https://blog.giersig.eu"` (placeholder) | `t("settingSiteUrlPlaceholder")` |
|
||||
| `"Sign in"` (btn) | `t("btnSignIn")` |
|
||||
| `"Enter your site URL first."` (Notice) | `t("noticeEnterSiteUrl")` |
|
||||
| `"Opening browser…"` (btn) | `t("btnOpeningBrowser")` |
|
||||
| `` `✅ Signed in as ${result.me}` `` | `t("noticeSignedInAs", { me: result.me })` |
|
||||
| `` `Sign-in failed: ${String(err)}` `` | `t("noticeSignInFailed", { error: String(err) })` |
|
||||
| `"Sign in"` (catch btn reset) | `t("btnSignIn")` |
|
||||
| `"Or paste a token manually"` (summary) | `t("manualTokenSummary")` |
|
||||
| `"Access token"` (setName) | `t("settingAccessToken")` |
|
||||
| `"Bearer token from your Indiekit admin panel."` (setDesc) | `t("settingAccessTokenDesc")` |
|
||||
| `"Verify"` (btn) | `t("btnVerify")` |
|
||||
| `"Set the Micropub endpoint and token first."` (Notice) | `t("noticeSetEndpointFirst")` |
|
||||
| `"✅ Token is valid!"` (Notice) | `t("noticeTokenValid")` |
|
||||
| `` `Token check failed: ${String(err)}` `` | `t("noticeTokenCheckFailed", { error: String(err) })` |
|
||||
|
||||
- [ ] **Step 4: Replace strings in `renderSignedIn()`**
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| `"Signed in"` (label) | `t("lblSignedIn")` |
|
||||
| `"Site URL"` (setName) | `t("settingSiteUrl")` |
|
||||
| `"Sign out"` (btn) | `t("btnSignOut")` |
|
||||
|
||||
- [ ] **Step 5: Build**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
|
||||
```
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
|
||||
git add src/SettingsTab.ts
|
||||
git commit -m "feat(i18n): wire t() into SettingsTab.ts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Wire `t()` into `SyndicationDialog.ts` and `IndieAuth.ts`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/SyndicationDialog.ts`
|
||||
- Modify: `src/IndieAuth.ts`
|
||||
|
||||
- [ ] **Step 1: Update `SyndicationDialog.ts`**
|
||||
|
||||
Add import:
|
||||
```typescript
|
||||
import { t } from "./i18n";
|
||||
```
|
||||
|
||||
Replace:
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| `"Syndication targets"` (h2) | `t("syndDialogTitle")` |
|
||||
| `"Choose where to cross-post this note."` (p) | `t("syndDialogSubtitle")` |
|
||||
| `"Cancel"` (btn) | `t("btnCancel")` |
|
||||
| `"Publish"` (btn) | `t("btnPublish")` |
|
||||
|
||||
- [ ] **Step 2: Update `IndieAuth.ts`**
|
||||
|
||||
Add import at top:
|
||||
```typescript
|
||||
import { t } from "./i18n";
|
||||
```
|
||||
|
||||
Replace:
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| `"Sign-in timed out (5 min). Please try again."` | `t("errSignInTimeout")` |
|
||||
|
||||
- [ ] **Step 3: Build**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub" && npm run build
|
||||
```
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
|
||||
git add src/SyndicationDialog.ts src/IndieAuth.ts
|
||||
git commit -m "feat(i18n): wire t() into SyndicationDialog and IndieAuth"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: README update
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: Add i18n note to Configuration section**
|
||||
|
||||
After the Settings reference table, add:
|
||||
|
||||
```markdown
|
||||
### Language
|
||||
|
||||
The plugin follows Obsidian's display language. Set it in **Settings → About → Language**. Currently supported: English (`en`), German (`de`).
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `SyndicationDialog.ts` already done in prior session (verify)**
|
||||
|
||||
Architecture table should already show `SyndicationDialog.ts` from the prior README update.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd "/Users/sven/PARA/1. Projects/obsidian-micropub"
|
||||
git add README.md
|
||||
git commit -m "docs: note i18n support (en/de) in README"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manual verification checklist
|
||||
|
||||
After all tasks, load the plugin in Obsidian and verify:
|
||||
|
||||
- [ ] With Obsidian in English: all UI labels, notices, and dialog text appear in English
|
||||
- [ ] With Obsidian in German (`de`): all UI labels, notices, and dialog text appear in German
|
||||
- [ ] Publishing a note shows the correct locale notice (`Publishing…` / `Wird veröffentlicht…`)
|
||||
- [ ] The syndication dialog title and buttons are translated
|
||||
- [ ] Sign-in flow button text and error notices are translated
|
||||
- [ ] Switching locale and restarting Obsidian picks up the new language
|
||||
@@ -1,217 +0,0 @@
|
||||
# Syndication Dialog Design
|
||||
|
||||
**Date:** 2026-03-30
|
||||
**Status:** Approved
|
||||
**Scope:** obsidian-micropub plugin feature
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Add a dialog that appears when publishing to Micropub, allowing users to select which syndication targets (e.g., Twitter, Mastodon) to cross-post to. The dialog integrates with the existing `?q=config` Micropub endpoint to fetch available targets.
|
||||
|
||||
---
|
||||
|
||||
## 2. User Flow
|
||||
|
||||
1. User clicks "Publish to Micropub"
|
||||
2. Plugin fetches `?q=config` to get available syndication targets
|
||||
3. Plugin checks frontmatter for `mp-syndicate-to`:
|
||||
- **Has values** → use those, skip dialog, publish
|
||||
- **Empty array `[]`** → force dialog
|
||||
- **Absent** → show dialog with defaults pre-checked
|
||||
4. Dialog displays checkboxes for each target from server
|
||||
5. User confirms → publish with selected targets
|
||||
6. Successful publish writes `mp-syndicate-to` to frontmatter
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration
|
||||
|
||||
### New Settings
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `showSyndicationDialog` | enum | `"when-needed"` | When to show the dialog |
|
||||
| `defaultSyndicateTo` | string[] | `[]` | Targets checked by default |
|
||||
|
||||
### `showSyndicationDialog` Options
|
||||
|
||||
- `"when-needed"` — Show only if `mp-syndicate-to` is absent from frontmatter
|
||||
- `"always"` — Show every time user publishes
|
||||
- `"never"` — Use defaults, never show dialog
|
||||
|
||||
### Frontmatter Support
|
||||
|
||||
Users can bypass the dialog per-note using frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
# Skip dialog, auto-syndicate to these targets
|
||||
mp-syndicate-to: [twitter, mastodon]
|
||||
|
||||
# Force dialog even with defaults set
|
||||
mp-syndicate-to: []
|
||||
---
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Components
|
||||
|
||||
### 4.1 SyndicationDialog (New)
|
||||
|
||||
**Location:** `src/SyndicationDialog.ts`
|
||||
|
||||
**Responsibilities:**
|
||||
- Render modal with checkbox list of targets
|
||||
- Pre-check targets from `defaultSyndicateTo` setting
|
||||
- Handle OK/Cancel actions
|
||||
- Return selected target UIDs via promise
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
export class SyndicationDialog extends Modal {
|
||||
constructor(
|
||||
app: App,
|
||||
targets: SyndicationTarget[],
|
||||
defaultSelected: string[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens the dialog and waits for user selection.
|
||||
* @returns Selected target UIDs, or null if cancelled.
|
||||
*/
|
||||
async awaitSelection(): Promise<string[] | null>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Publisher (Modified)
|
||||
|
||||
**Changes:**
|
||||
- Accept optional `syndicateToOverride?: string[]` parameter
|
||||
- Merge override with frontmatter values (override wins)
|
||||
- Write `mp-syndicate-to` to frontmatter on successful publish
|
||||
|
||||
### 4.3 SettingsTab (Modified)
|
||||
|
||||
**Changes:**
|
||||
- Add dropdown for `showSyndicationDialog` behavior
|
||||
- Display currently configured default targets (read-only list)
|
||||
- Add button to clear defaults
|
||||
|
||||
### 4.4 main.ts (Modified)
|
||||
|
||||
**Changes:**
|
||||
- Before calling `publishActiveNote`:
|
||||
1. Fetch `?q=config` for syndication targets
|
||||
2. Check frontmatter for `mp-syndicate-to`
|
||||
3. Decide whether to show dialog based on setting + frontmatter
|
||||
4. If showing dialog, wait for user selection
|
||||
5. Call `publisher.publish()` with selected targets
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow
|
||||
|
||||
```
|
||||
User clicks "Publish"
|
||||
│
|
||||
▼
|
||||
Fetch ?q=config ──► Check frontmatter mp-syndicate-to
|
||||
│ │
|
||||
│ ┌─────────────┼─────────────┐
|
||||
│ │ │ │
|
||||
│ Has values Absent Empty []
|
||||
│ (skip dialog) (show dialog) (show dialog)
|
||||
│ │ │ │
|
||||
│ └─────────────┴─────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
SyndicationDialog (if needed)
|
||||
│
|
||||
▼
|
||||
Publisher.publish(selectedTargets?)
|
||||
│
|
||||
▼
|
||||
Write mp-syndicate-to to frontmatter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Error Handling
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| `?q=config` fails | Warn user, offer to publish without syndication or cancel |
|
||||
| Dialog canceled | Abort publish, no changes |
|
||||
| Micropub POST fails | Don't write `mp-syndicate-to` to frontmatter |
|
||||
| No targets returned from server | Skip dialog, publish normally (backward compatible) |
|
||||
|
||||
---
|
||||
|
||||
## 7. UI/UX Details
|
||||
|
||||
### Dialog Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Publish to Syndication Targets │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [✓] Twitter (@username) │
|
||||
│ [✓] Mastodon (@user@instance) │
|
||||
│ [ ] LinkedIn │
|
||||
│ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ [Cancel] [Publish] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Settings UI Addition
|
||||
|
||||
```
|
||||
Publish Behaviour
|
||||
├── Default visibility: [public ▼]
|
||||
├── Write URL back to note: [✓]
|
||||
├── Syndication dialog: [when-needed ▼]
|
||||
│ └── when-needed: Show only if no mp-syndicate-to
|
||||
├── Default syndication targets:
|
||||
│ └── twitter, mastodon [Clear defaults]
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Edge Cases
|
||||
|
||||
1. **User has no syndication targets configured on server** — Skip dialog, publish normally
|
||||
2. **User cancels dialog** — Abort publish entirely, no state changes
|
||||
3. **Micropub server returns targets but some are invalid** — Show all, let server reject invalid ones
|
||||
4. **User changes targets in settings after publishing** — Affects future publishes only, doesn't retroactively change existing `mp-syndicate-to` frontmatter
|
||||
|
||||
---
|
||||
|
||||
## 9. Backward Compatibility
|
||||
|
||||
- Default `showSyndicationDialog: "when-needed"` means existing behavior unchanged for notes without frontmatter
|
||||
- Existing `mp-syndicate-to` frontmatter values continue to work
|
||||
- Plugin remains compatible with servers that don't return syndication targets
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing Considerations
|
||||
|
||||
- Unit test: `SyndicationDialog` renders checkboxes correctly
|
||||
- Unit test: Frontmatter parsing handles `mp-syndicate-to` array
|
||||
- Unit test: Setting `"never"` skips dialog
|
||||
- Integration test: Full flow from click to publish with targets
|
||||
- Edge case: Server returns empty targets array
|
||||
- Edge case: User cancels dialog
|
||||
|
||||
---
|
||||
|
||||
## Approval
|
||||
|
||||
**Approved by:** @svemagie
|
||||
**Date:** 2026-03-30
|
||||
Reference in New Issue
Block a user