feat: i18n support with first translations

This commit is contained in:
2026-02-21 10:45:41 +01:00
parent a5281a7750
commit b8005bec30
48 changed files with 2792 additions and 462 deletions

View File

@@ -5,6 +5,7 @@ import type { MediaData } from './MediaEngine';
import type { PostData } from './PostEngine';
import { PICO_THEME_NAMES } from '../shared/picoThemes';
import { TAG_CLOUD_RUNTIME_JS } from './assets/tagCloudRuntime';
import { resolveRenderLanguageFromProjectPreferences, translateRender } from '../shared/i18n';
export interface HtmlRewriteContext {
canonicalPostPathBySlug: Map<string, string>;
@@ -87,6 +88,8 @@ export interface SinglePostTemplateContext {
export interface NotFoundTemplateContext {
page_title: string;
language: string;
not_found_message?: string;
not_found_back_label?: string;
pico_stylesheet_href?: string;
html_theme_attribute?: string;
}
@@ -336,7 +339,9 @@ export function renderGalleryMacro(
postId: string,
mediaItems: MediaData[],
linkedMediaIds: Set<string> | null,
renderLanguage: string,
): string {
const language = resolveRenderLanguageFromProjectPreferences(renderLanguage);
const requestedColumns = parseIntegerParam(params.columns);
const columns = requestedColumns && requestedColumns >= 1 && requestedColumns <= 6 ? requestedColumns : 3;
const caption = params.caption ? `<figcaption class="gallery-caption">${escapeHtml(params.caption)}</figcaption>` : '';
@@ -361,12 +366,16 @@ export function renderGalleryMacro(
return `<a class="gallery-item" href="${mediaPath}" data-lightbox="${groupName}" data-title="${title}"><img src="${mediaPath}" alt="${alt}" loading="lazy" /></a>`;
}).join('');
const content = galleryItems || '<div class="gallery-empty">No linked images found.</div>';
const content = galleryItems || `<div class="gallery-empty">${escapeHtml(translateRender(language, 'render.gallery.empty'))}</div>`;
return `<div class="macro-gallery gallery-cols-${columns}" data-post-id="${escapeHtml(postId)}" data-columns="${columns}" data-lightbox="true"><div class="gallery-container gallery-lightbox">${content}</div>${caption}</div>`;
}
export function renderPhotoArchiveMacro(params: Record<string, string>, mediaItems: MediaData[]): string {
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
export function renderPhotoArchiveMacro(
params: Record<string, string>,
mediaItems: MediaData[],
renderLanguage: string,
): string {
const language = resolveRenderLanguageFromProjectPreferences(renderLanguage);
const yearParam = parseIntegerParam(params.year);
const monthParam = parseIntegerParam(params.month);
@@ -393,11 +402,12 @@ export function renderPhotoArchiveMacro(params: Record<string, string>, mediaIte
const buckets = buildPhotoArchiveBuckets(renderableMedia, params);
if (buckets.length === 0) {
return `<div class="${rootClasses.join(' ')}" ${dataAttrs.join(' ')}><div class="photo-archive-container"><div class="photo-archive-empty">No photos found for this archive.</div></div></div>`;
const emptyLabel = escapeHtml(translateRender(language, 'render.photoArchive.empty'));
return `<div class="${rootClasses.join(' ')}" ${dataAttrs.join(' ')}><div class="photo-archive-container"><div class="photo-archive-empty">${emptyLabel}</div></div></div>`;
}
const monthsHtml = buckets.map((bucket) => {
const monthName = monthNames[bucket.month - 1] || String(bucket.month);
const monthName = translateRender(language, `render.month.${bucket.month}`);
const label = `${monthName} ${bucket.year}`;
const groupName = `photo-archive-${bucket.year}-${String(bucket.month).padStart(2, '0')}`;
@@ -414,7 +424,8 @@ export function renderPhotoArchiveMacro(params: Record<string, string>, mediaIte
return `<div class="${rootClasses.join(' ')}" ${dataAttrs.join(' ')}><div class="photo-archive-container">${monthsHtml}</div></div>`;
}
export function renderTagCloudMacro(params: Record<string, string>, tagUsage: TagUsageEntry[]): string {
export function renderTagCloudMacro(params: Record<string, string>, tagUsage: TagUsageEntry[], renderLanguage: string): string {
const language = resolveRenderLanguageFromProjectPreferences(renderLanguage);
const widthParam = parseIntegerParam(params.width);
const heightParam = parseIntegerParam(params.height);
const orientation = normalizeTagCloudOrientation(params.orientation);
@@ -422,7 +433,7 @@ export function renderTagCloudMacro(params: Record<string, string>, tagUsage: Ta
const height = heightParam && heightParam >= 180 && heightParam <= 900 ? heightParam : 420;
if (tagUsage.length === 0) {
return `<div class="macro-tag-cloud" data-tag-cloud="true" data-orientation="${orientation}" data-color-distribution="quantile" data-color-easing="0.7" data-color-theme="pico"><div class="tag-cloud-empty">No tags found.</div></div>`;
return `<div class="macro-tag-cloud" data-tag-cloud="true" data-orientation="${orientation}" data-color-distribution="quantile" data-color-easing="0.7" data-color-theme="pico"><div class="tag-cloud-empty">${escapeHtml(translateRender(language, 'render.tagCloud.empty'))}</div></div>`;
}
const minCount = Math.min(...tagUsage.map((entry) => entry.count));
@@ -445,7 +456,8 @@ export function renderTagCloudMacro(params: Record<string, string>, tagUsage: Ta
const wordsJson = escapeHtml(JSON.stringify(words));
return `<div class="macro-tag-cloud" data-tag-cloud="true" data-orientation="${orientation}" data-color-distribution="quantile" data-color-easing="0.7" data-color-theme="pico" data-tag-cloud-words="${wordsJson}" data-width="${width}" data-height="${height}"><svg class="tag-cloud-canvas" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet" aria-label="Tag cloud"></svg></div>`;
const ariaLabel = escapeHtml(translateRender(language, 'render.tagCloud.ariaLabel'));
return `<div class="macro-tag-cloud" data-tag-cloud="true" data-orientation="${orientation}" data-color-distribution="quantile" data-color-easing="0.7" data-color-theme="pico" data-tag-cloud-words="${wordsJson}" data-width="${width}" data-height="${height}"><svg class="tag-cloud-canvas" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet" aria-label="${ariaLabel}"></svg></div>`;
}
export function isExternalOrSpecialUrl(value: string): boolean {
@@ -551,33 +563,36 @@ export function renderMacro(
mediaItems: MediaData[],
linkedMediaIds: Set<string> | null,
tagUsage: TagUsageEntry[],
renderLanguage: string,
): string {
const normalizedName = normalizeMacroName(name);
if (normalizedName === 'youtube') {
const language = resolveRenderLanguageFromProjectPreferences(renderLanguage);
const id = escapeHtml(params.id || '');
const title = escapeHtml(params.title || 'YouTube video');
const title = escapeHtml(params.title || translateRender(language, 'render.video.youtubeTitle'));
if (!id) return '';
return `<div class="macro-youtube"><iframe src="https://www.youtube.com/embed/${id}?rel=0" title="${title}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
}
if (normalizedName === 'vimeo') {
const language = resolveRenderLanguageFromProjectPreferences(renderLanguage);
const id = escapeHtml(params.id || '');
const title = escapeHtml(params.title || 'Vimeo video');
const title = escapeHtml(params.title || translateRender(language, 'render.video.vimeoTitle'));
if (!id) return '';
return `<div class="macro-vimeo"><iframe src="https://player.vimeo.com/video/${id}" title="${title}" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe></div>`;
}
if (normalizedName === 'gallery') {
return renderGalleryMacro(params, postId, mediaItems, linkedMediaIds);
return renderGalleryMacro(params, postId, mediaItems, linkedMediaIds, renderLanguage);
}
if (normalizedName === 'photo_archive') {
return renderPhotoArchiveMacro(params, mediaItems);
return renderPhotoArchiveMacro(params, mediaItems, renderLanguage);
}
if (normalizedName === 'tag_cloud') {
return renderTagCloudMacro(params, tagUsage);
return renderTagCloudMacro(params, tagUsage, renderLanguage);
}
return '';
@@ -677,9 +692,23 @@ export class PageRenderer {
cache: true,
});
this.liquid.registerFilter('markdown', async (value: unknown, postIdArg: unknown, canonicalPostsArg: unknown, canonicalMediaArg: unknown) => {
this.liquid.registerFilter('i18n', (keyArg: unknown, renderLanguageArg: unknown) => {
const key = typeof keyArg === 'string' ? keyArg : '';
const resolved = resolveRenderLanguageFromProjectPreferences(
typeof renderLanguageArg === 'string' ? renderLanguageArg : 'en',
);
if (!key) {
return '';
}
return translateRender(resolved, key);
});
this.liquid.registerFilter('markdown', async (value: unknown, postIdArg: unknown, canonicalPostsArg: unknown, canonicalMediaArg: unknown, renderLanguageArg: unknown) => {
const content = typeof value === 'string' ? value : '';
const postId = typeof postIdArg === 'string' ? postIdArg : '';
const renderLanguage = typeof renderLanguageArg === 'string' ? renderLanguageArg : 'en';
const rewriteContext: HtmlRewriteContext = {
canonicalPostPathBySlug: recordToMap(canonicalPostsArg),
canonicalMediaPathBySourcePath: recordToMap(canonicalMediaArg),
@@ -703,7 +732,7 @@ export class PageRenderer {
const withMacros = content.replace(/\[\[(\w+)(?:\s+([^\]]+))?\]\]/g, (_match, macroName: string, rawParams: string | undefined) => {
const params = parseMacroParams(rawParams);
return renderMacro(macroName.toLowerCase(), params, postId, mediaItems, linkedMediaIds, tagUsage);
return renderMacro(macroName.toLowerCase(), params, postId, mediaItems, linkedMediaIds, tagUsage, renderLanguage);
});
const markdownHtml = await marked.parse(withMacros, { async: true, gfm: true, breaks: false });
@@ -964,6 +993,10 @@ export class PageRenderer {
}
async renderNotFound(context: NotFoundTemplateContext): Promise<string> {
return this.liquid.renderFile('not-found', context);
return this.liquid.renderFile('not-found', {
...context,
not_found_message: context.not_found_message,
not_found_back_label: context.not_found_back_label,
});
}
}

View File

@@ -6,8 +6,10 @@
<section class="not-found" data-template="not-found">
<article>
<h1>404</h1>
<p>The requested preview page could not be found.</p>
<p><a href="/" role="button">Back to preview home</a></p>
{% assign default_not_found_message = 'render.notFound.message' | i18n: language %}
{% assign default_not_found_back = 'render.notFound.back' | i18n: language %}
<p>{{ not_found_message | default: default_not_found_message }}</p>
<p><a href="/" role="button">{{ not_found_back_label | default: default_not_found_back }}</a></p>
</article>
</section>
</main>

View File

@@ -4,26 +4,23 @@
<body>
<main>
{% if archive_context %}
{% assign month_names_de = "Januar|Februar|März|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember" | split: "|" %}
{% if show_archive_range_heading and min_date and max_date %}
{% if archive_context.kind == 'tag' or archive_context.kind == 'category' %}
<h1 class="archive-heading">{{ archive_context.name }} - {{ min_date.day }}.{{ min_date.month }}.{{ min_date.year }} - {{ max_date.day }}.{{ max_date.month }}.{{ max_date.year }}</h1>
{% else %}
<h1 class="archive-heading">Archiv {{ min_date.day }}.{{ min_date.month }}.{{ min_date.year }} - {{ max_date.day }}.{{ max_date.month }}.{{ max_date.year }}</h1>
<h1 class="archive-heading">{{ 'render.archive' | i18n: language }} {{ min_date.day }}.{{ min_date.month }}.{{ min_date.year }} - {{ max_date.day }}.{{ max_date.month }}.{{ max_date.year }}</h1>
{% endif %}
{% else %}
{% if archive_context.kind == 'tag' or archive_context.kind == 'category' %}
<h1 class="archive-heading">{{ archive_context.name }}</h1>
{% elsif archive_context.kind == 'month' and archive_context.month and archive_context.year %}
{% assign month_index = archive_context.month | minus: 1 %}
{% assign month_name = month_names_de[month_index] %}
<h1 class="archive-heading">Archiv {{ month_name }} {{ archive_context.year }}</h1>
{% assign month_key = 'render.month.' | append: archive_context.month %}
<h1 class="archive-heading">{{ 'render.archive' | i18n: language }} {{ month_key | i18n: language }} {{ archive_context.year }}</h1>
{% elsif archive_context.kind == 'year' and archive_context.year %}
<h1 class="archive-heading">Archiv {{ archive_context.year }}</h1>
<h1 class="archive-heading">{{ 'render.archive' | i18n: language }} {{ archive_context.year }}</h1>
{% elsif archive_context.kind == 'day' and archive_context.day and archive_context.month and archive_context.year %}
{% assign day_month_index = archive_context.month | minus: 1 %}
{% assign day_month_name = month_names_de[day_month_index] %}
<h1 class="archive-heading">Archiv {{ archive_context.day }}. {{ day_month_name }} {{ archive_context.year }}</h1>
{% assign day_month_key = 'render.month.' | append: archive_context.month %}
<h1 class="archive-heading">{{ 'render.archive' | i18n: language }} {{ archive_context.day }}. {{ day_month_key | i18n: language }} {{ archive_context.year }}</h1>
{% else %}
<h1 class="archive-heading">{{ page_title }}</h1>
{% endif %}
@@ -41,7 +38,7 @@
{% if post.show_title %}
<h2 class="post-title">{{ post.title }}</h2>
{% endif %}
{{ post.content | markdown: post.id, canonical_post_path_by_slug, canonical_media_path_by_source_path }}
{{ post.content | markdown: post.id, canonical_post_path_by_slug, canonical_media_path_by_source_path, language }}
</div>
{% endfor %}
</div>
@@ -52,7 +49,7 @@
{% if post.show_title %}
<h2 class="post-title">{{ post.title }}</h2>
{% endif %}
{{ post.content | markdown: post.id, canonical_post_path_by_slug, canonical_media_path_by_source_path }}
{{ post.content | markdown: post.id, canonical_post_path_by_slug, canonical_media_path_by_source_path, language }}
</div>
{% endfor %}
{% endif %}
@@ -64,15 +61,15 @@
</section>
{% if has_prev_page or has_next_page %}
<nav class="preview-pagination" aria-label="Pagination">
<nav class="preview-pagination" aria-label="{{ 'render.pagination.label' | i18n: language }}">
{% if has_prev_page %}
<a href="{{ prev_page_href }}" class="preview-pagination-link" aria-label="Newer posts">neuer</a>
<a href="{{ prev_page_href }}" class="preview-pagination-link" aria-label="{{ 'render.pagination.newer' | i18n: language }}">{{ 'render.pagination.newer' | i18n: language }}</a>
{% else %}
<span class="spacer"></span>
{% endif %}
{% if has_next_page %}
<a href="{{ next_page_href }}" class="preview-pagination-link" aria-label="Older posts">älter</a>
<a href="{{ next_page_href }}" class="preview-pagination-link" aria-label="{{ 'render.pagination.older' | i18n: language }}">{{ 'render.pagination.older' | i18n: language }}</a>
{% else %}
<span class="spacer"></span>
{% endif %}

View File

@@ -5,7 +5,7 @@
<main>
<article class="single-post" data-template="single-post">
<h1>{{ post.title }}</h1>
<div class="post">{{ post.content | markdown: post.id, canonical_post_path_by_slug, canonical_media_path_by_source_path }}</div>
<div class="post">{{ post.content | markdown: post.id, canonical_post_path_by_slug, canonical_media_path_by_source_path, language }}</div>
</article>
</main>
</body>

View File

@@ -222,6 +222,10 @@ export function registerIpcHandlers(): void {
return engine.deleteProjectWithData(id);
});
safeHandle('app:getSystemLanguage', async () => {
return app.getLocale();
});
safeHandle('projects:get', async (_, id: string) => {
const engine = getProjectEngine();
return engine.getProject(id);

View File

@@ -9,6 +9,7 @@ import { getMediaEngine } from './engine/MediaEngine';
import { getPostEngine } from './engine/PostEngine';
import { PreviewServer } from './engine/PreviewServer';
import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_GROUPS, APP_MENU_ITEM_IDS, type AppMenuAction, type AppMenuItemDefinition } from './shared/menuCommands';
import { resolveUiLanguageFromSystemLocale, translateMenu } from './shared/i18n';
let mainWindow: BrowserWindow | null = null;
let previewServer: PreviewServer | null = null;
@@ -170,6 +171,8 @@ async function startPreviewServerOnAppStart(): Promise<void> {
}
function createApplicationMenu(): Menu {
const systemLocale = typeof app.getLocale === 'function' ? app.getLocale() : 'en';
const uiLanguage = resolveUiLanguageFromSystemLocale(systemLocale);
const commandDefinitions = APP_MENU_GROUPS
.flatMap(group => group.items)
.filter(item => !item.separator)
@@ -258,22 +261,32 @@ function createApplicationMenu(): Menu {
}
};
const getMenuItemLabel = (action: AppMenuAction, fallback: string): string => {
return translateMenu(uiLanguage, `menu.item.${action}`) || fallback;
};
const getMenuGroupLabel = (groupLabel: string): string => {
return translateMenu(uiLanguage, `menu.group.${groupLabel.toLowerCase()}`) || groupLabel;
};
const buildSharedMenuItem = (action: AppMenuAction): MenuItemConstructorOptions => {
const definition = commandDefinitions[action];
if (!definition) {
throw new Error(`Unknown shared menu action: ${action}`);
}
const translatedLabel = getMenuItemLabel(action, definition.label);
if (definition.role) {
return {
label: definition.label,
label: translatedLabel,
role: definition.role,
accelerator: definition.accelerator,
};
}
return {
label: definition.label,
label: translatedLabel,
accelerator: definition.accelerator,
id: definition.id,
enabled: definition.enabled,
@@ -302,23 +315,23 @@ function createApplicationMenu(): Menu {
const template: MenuItemConstructorOptions[] = [
{
label: 'File',
label: getMenuGroupLabel('File'),
submenu: buildSharedGroupMenuItems('File'),
},
{
label: 'Edit',
label: getMenuGroupLabel('Edit'),
submenu: buildSharedGroupMenuItems('Edit'),
},
{
label: 'View',
label: getMenuGroupLabel('View'),
submenu: buildSharedGroupMenuItems('View'),
},
{
label: 'Blog',
label: getMenuGroupLabel('Blog'),
submenu: buildSharedGroupMenuItems('Blog'),
},
{
label: 'Help',
label: getMenuGroupLabel('Help'),
submenu: buildSharedGroupMenuItems('Help'),
},
];

View File

@@ -137,6 +137,7 @@ export const electronAPI: ElectronAPI = {
// App
app: {
getDataPaths: () => ipcRenderer.invoke('app:getDataPaths'),
getSystemLanguage: () => ipcRenderer.invoke('app:getSystemLanguage'),
getTitleBarMetrics: () => ipcRenderer.invoke('app:getTitleBarMetrics'),
openFolder: (folderPath: string) => ipcRenderer.invoke('app:openFolder', folderPath),
showItemInFolder: (itemPath: string) => ipcRenderer.invoke('app:showItemInFolder', itemPath),

View File

@@ -516,6 +516,7 @@ export interface ElectronAPI {
};
app: {
getDataPaths: () => Promise<{ database: string; posts: string; media: string }>;
getSystemLanguage: () => Promise<string>;
getTitleBarMetrics: () => Promise<{ macosLeftInset: number } | null>;
openFolder: (folderPath: string) => Promise<string>;
showItemInFolder: (itemPath: string) => Promise<void>;

55
src/main/shared/i18n.ts Normal file
View File

@@ -0,0 +1,55 @@
import enJson from './i18n/locales/en.json';
import deJson from './i18n/locales/de.json';
import frJson from './i18n/locales/fr.json';
import itJson from './i18n/locales/it.json';
import esJson from './i18n/locales/es.json';
export type SupportedLanguage = 'en' | 'de' | 'fr' | 'it' | 'es';
type TranslationMap = Record<string, string>;
const en = enJson as TranslationMap;
const de = { ...en, ...(deJson as TranslationMap) };
const fr = { ...en, ...(frJson as TranslationMap) };
const it = { ...en, ...(itJson as TranslationMap) };
const es = { ...en, ...(esJson as TranslationMap) };
const catalog: Record<SupportedLanguage, TranslationMap> = { en, de, fr, it, es };
function normalizeLanguage(input: string | undefined | null): SupportedLanguage {
const normalized = (input || '').trim().toLowerCase();
if (!normalized) {
return 'en';
}
const base = normalized.split('-')[0];
if (base === 'en' || base === 'de' || base === 'fr' || base === 'it' || base === 'es') {
return base;
}
return 'en';
}
export function resolveSupportedRenderLanguage(language: string | undefined | null): SupportedLanguage {
return normalizeLanguage(language);
}
export function resolveRenderLanguageFromProjectPreferences(mainLanguage: string | undefined | null): SupportedLanguage {
return normalizeLanguage(mainLanguage);
}
export function resolveSupportedUiLanguage(language: string | undefined | null): SupportedLanguage {
return normalizeLanguage(language);
}
export function resolveUiLanguageFromSystemLocale(systemLocale: string | undefined | null): SupportedLanguage {
return normalizeLanguage(systemLocale);
}
export function translateRender(language: SupportedLanguage, key: string): string {
return catalog[language]?.[key] ?? catalog.en[key] ?? key;
}
export function translateMenu(language: SupportedLanguage, key: string): string {
return catalog[language]?.[key] ?? catalog.en[key] ?? key;
}

View File

@@ -0,0 +1,67 @@
{
"menu.group.file": "Datei",
"menu.group.edit": "Bearbeiten",
"menu.group.view": "Ansicht",
"menu.group.blog": "Blogbereich",
"menu.group.help": "Hilfe",
"menu.item.newPost": "Neuer Beitrag",
"menu.item.importMedia": "Medien importieren...",
"menu.item.save": "Speichern",
"menu.item.openInBrowser": "Im Browser öffnen",
"menu.item.openDataFolder": "Datenordner öffnen",
"menu.item.quit": "Beenden",
"menu.item.undo": "Rückgängig",
"menu.item.redo": "Wiederholen",
"menu.item.cut": "Ausschneiden",
"menu.item.copy": "Kopieren",
"menu.item.paste": "Einfügen",
"menu.item.delete": "Löschen",
"menu.item.selectAll": "Alles auswählen",
"menu.item.find": "Suchen",
"menu.item.replace": "Ersetzen",
"menu.item.viewPosts": "Beiträge",
"menu.item.viewMedia": "Medien",
"menu.item.toggleSidebar": "Seitenleiste umschalten",
"menu.item.togglePanel": "Panel umschalten",
"menu.item.toggleDevTools": "Entwicklerwerkzeuge umschalten",
"menu.item.reload": "Neu laden",
"menu.item.forceReload": "Erzwungen neu laden",
"menu.item.resetZoom": "Tatsächliche Größe",
"menu.item.zoomIn": "Vergrößern",
"menu.item.zoomOut": "Verkleinern",
"menu.item.toggleFullScreen": "Vollbild umschalten",
"menu.item.publishSelected": "Ausgewählte veröffentlichen",
"menu.item.previewPost": "Beitragsvorschau",
"menu.item.rebuildDatabase": "Datenbank aus Dateien neu aufbauen",
"menu.item.reindexText": "Suchtext neu indizieren",
"menu.item.metadataDiff": "Metadaten-Diff-Werkzeug",
"menu.item.generateSitemap": "Site rendern",
"menu.item.about": "Über Blogging Desktop Server",
"menu.item.openDocumentation": "Dokumentation öffnen",
"menu.item.viewOnGitHub": "Auf GitHub ansehen",
"menu.item.reportIssue": "Problem melden",
"render.archive": "Archiv",
"render.pagination.label": "Seitennummerierung",
"render.pagination.newer": "neuer",
"render.pagination.older": "älter",
"render.notFound.message": "Die angeforderte Vorschauseite konnte nicht gefunden werden.",
"render.notFound.back": "Zurück zur Vorschau-Startseite",
"render.photoArchive.empty": "Keine Fotos für dieses Archiv gefunden.",
"render.gallery.empty": "Keine verknüpften Bilder gefunden.",
"render.tagCloud.empty": "Keine Tags gefunden.",
"render.tagCloud.ariaLabel": "Tag-Wolke",
"render.video.youtubeTitle": "YouTube-Video",
"render.video.vimeoTitle": "Vimeo-Video",
"render.month.1": "Januar",
"render.month.2": "Februar",
"render.month.3": "März",
"render.month.4": "Apr.",
"render.month.5": "Mai",
"render.month.6": "Juni",
"render.month.7": "Juli",
"render.month.8": "Aug.",
"render.month.9": "Sept.",
"render.month.10": "Oktober",
"render.month.11": "Nov.",
"render.month.12": "Dezember"
}

View File

@@ -0,0 +1,67 @@
{
"menu.group.file": "File",
"menu.group.edit": "Edit",
"menu.group.view": "View",
"menu.group.blog": "Blog",
"menu.group.help": "Help",
"menu.item.newPost": "New Post",
"menu.item.importMedia": "Import Media...",
"menu.item.save": "Save",
"menu.item.openInBrowser": "Open in Browser",
"menu.item.openDataFolder": "Open Data Folder",
"menu.item.quit": "Quit",
"menu.item.undo": "Undo",
"menu.item.redo": "Redo",
"menu.item.cut": "Cut",
"menu.item.copy": "Copy",
"menu.item.paste": "Paste",
"menu.item.delete": "Delete",
"menu.item.selectAll": "Select All",
"menu.item.find": "Find",
"menu.item.replace": "Replace",
"menu.item.viewPosts": "Posts",
"menu.item.viewMedia": "Media",
"menu.item.toggleSidebar": "Toggle Sidebar",
"menu.item.togglePanel": "Toggle Panel",
"menu.item.toggleDevTools": "Toggle Developer Tools",
"menu.item.reload": "Reload",
"menu.item.forceReload": "Force Reload",
"menu.item.resetZoom": "Actual Size",
"menu.item.zoomIn": "Zoom In",
"menu.item.zoomOut": "Zoom Out",
"menu.item.toggleFullScreen": "Toggle Full Screen",
"menu.item.publishSelected": "Publish Selected",
"menu.item.previewPost": "Preview Post",
"menu.item.rebuildDatabase": "Rebuild Database from Files",
"menu.item.reindexText": "Reindex Search Text",
"menu.item.metadataDiff": "Metadata Diff Tool",
"menu.item.generateSitemap": "Render Site",
"menu.item.about": "About Blogging Desktop Server",
"menu.item.openDocumentation": "Open Documentation",
"menu.item.viewOnGitHub": "View on GitHub",
"menu.item.reportIssue": "Report Issue",
"render.archive": "Archive",
"render.pagination.label": "Pagination",
"render.pagination.newer": "newer",
"render.pagination.older": "older",
"render.notFound.message": "The requested preview page could not be found.",
"render.notFound.back": "Back to preview home",
"render.photoArchive.empty": "No photos found for this archive.",
"render.gallery.empty": "No linked images found.",
"render.tagCloud.empty": "No tags found.",
"render.tagCloud.ariaLabel": "Tag cloud",
"render.video.youtubeTitle": "YouTube video",
"render.video.vimeoTitle": "Vimeo video",
"render.month.1": "January",
"render.month.2": "February",
"render.month.3": "March",
"render.month.4": "April",
"render.month.5": "May",
"render.month.6": "June",
"render.month.7": "July",
"render.month.8": "August",
"render.month.9": "September",
"render.month.10": "October",
"render.month.11": "November",
"render.month.12": "December"
}

View File

@@ -0,0 +1,67 @@
{
"menu.group.file": "Archivo",
"menu.group.edit": "Editar",
"menu.group.view": "Ver",
"menu.group.blog": "Bitácora",
"menu.group.help": "Ayuda",
"menu.item.newPost": "Nueva entrada",
"menu.item.importMedia": "Importar medios...",
"menu.item.save": "Guardar",
"menu.item.openInBrowser": "Abrir en el navegador",
"menu.item.openDataFolder": "Abrir carpeta de datos",
"menu.item.quit": "Salir",
"menu.item.undo": "Deshacer",
"menu.item.redo": "Rehacer",
"menu.item.cut": "Cortar",
"menu.item.copy": "Copiar",
"menu.item.paste": "Pegar",
"menu.item.delete": "Eliminar",
"menu.item.selectAll": "Seleccionar todo",
"menu.item.find": "Buscar",
"menu.item.replace": "Reemplazar",
"menu.item.viewPosts": "Entradas",
"menu.item.viewMedia": "Medios",
"menu.item.toggleSidebar": "Alternar barra lateral",
"menu.item.togglePanel": "Alternar panel",
"menu.item.toggleDevTools": "Alternar herramientas de desarrollo",
"menu.item.reload": "Recargar",
"menu.item.forceReload": "Forzar recarga",
"menu.item.resetZoom": "Tamaño real",
"menu.item.zoomIn": "Acercar",
"menu.item.zoomOut": "Alejar",
"menu.item.toggleFullScreen": "Alternar pantalla completa",
"menu.item.publishSelected": "Publicar selección",
"menu.item.previewPost": "Vista previa de entrada",
"menu.item.rebuildDatabase": "Reconstruir Database from Files",
"menu.item.reindexText": "Reindex Buscar Text",
"menu.item.metadataDiff": "Herramienta diff de metadatos",
"menu.item.generateSitemap": "Renderizar sitio",
"menu.item.about": "Acerca de Blogging Desktop Server",
"menu.item.openDocumentation": "Abrir documentación",
"menu.item.viewOnGitHub": "Ver en GitHub",
"menu.item.reportIssue": "Reportar problema",
"render.archive": "Archivo",
"render.pagination.label": "Paginación",
"render.pagination.newer": "más reciente",
"render.pagination.older": "más antiguo",
"render.notFound.message": "No se pudo encontrar la página de vista previa solicitada.",
"render.notFound.back": "Volver al inicio de vista previa",
"render.photoArchive.empty": "No se encontraron fotos para este archivo.",
"render.gallery.empty": "No se encontraron imágenes vinculadas.",
"render.tagCloud.empty": "No se encontraron etiquetas.",
"render.tagCloud.ariaLabel": "Nube de etiquetas",
"render.video.youtubeTitle": "Vídeo de YouTube",
"render.video.vimeoTitle": "Vídeo de Vimeo",
"render.month.1": "enero",
"render.month.2": "febrero",
"render.month.3": "marzo",
"render.month.4": "abril",
"render.month.5": "mayo",
"render.month.6": "junio",
"render.month.7": "julio",
"render.month.8": "agosto",
"render.month.9": "septiembre",
"render.month.10": "octubre",
"render.month.11": "noviembre",
"render.month.12": "diciembre"
}

View File

@@ -0,0 +1,67 @@
{
"menu.group.file": "Fichier",
"menu.group.edit": "Édition",
"menu.group.view": "Affichage",
"menu.group.blog": "Espace blog",
"menu.group.help": "Aide",
"menu.item.newPost": "Nouvel article",
"menu.item.importMedia": "Importer des médias...",
"menu.item.save": "Enregistrer",
"menu.item.openInBrowser": "Ouvrir dans le navigateur",
"menu.item.openDataFolder": "Ouvrir le dossier de données",
"menu.item.quit": "Quitter",
"menu.item.undo": "Annuler",
"menu.item.redo": "Rétablir",
"menu.item.cut": "Couper",
"menu.item.copy": "Copier",
"menu.item.paste": "Coller",
"menu.item.delete": "Supprimer",
"menu.item.selectAll": "Tout sélectionner",
"menu.item.find": "Rechercher",
"menu.item.replace": "Remplacer",
"menu.item.viewPosts": "Articles",
"menu.item.viewMedia": "Médias",
"menu.item.toggleSidebar": "Basculer la barre latérale",
"menu.item.togglePanel": "Basculer le panneau",
"menu.item.toggleDevTools": "Basculer les outils de développement",
"menu.item.reload": "Recharger",
"menu.item.forceReload": "Forcer le rechargement",
"menu.item.resetZoom": "Taille réelle",
"menu.item.zoomIn": "Zoom avant",
"menu.item.zoomOut": "Zoom arrière",
"menu.item.toggleFullScreen": "Basculer en plein écran",
"menu.item.publishSelected": "Publier la sélection",
"menu.item.previewPost": "Aperçu de larticle",
"menu.item.rebuildDatabase": "Reconstruire Database from Files",
"menu.item.reindexText": "Reindex Recherche Text",
"menu.item.metadataDiff": "Outil de diff des métadonnées",
"menu.item.generateSitemap": "Rendre le site",
"menu.item.about": "À propos de Blogging Desktop Server",
"menu.item.openDocumentation": "Ouvrir la documentation",
"menu.item.viewOnGitHub": "Voir sur GitHub",
"menu.item.reportIssue": "Signaler un problème",
"render.archive": "Archives",
"render.pagination.label": "Navigation paginée",
"render.pagination.newer": "plus récent",
"render.pagination.older": "plus ancien",
"render.notFound.message": "La page daperçu demandée est introuvable.",
"render.notFound.back": "Retour à laccueil de laperçu",
"render.photoArchive.empty": "Aucune photo trouvée pour cette archive.",
"render.gallery.empty": "Aucune image liée trouvée.",
"render.tagCloud.empty": "Aucun tag trouvé.",
"render.tagCloud.ariaLabel": "Nuage de tags",
"render.video.youtubeTitle": "Vidéo YouTube",
"render.video.vimeoTitle": "Vidéo Vimeo",
"render.month.1": "janvier",
"render.month.2": "février",
"render.month.3": "mars",
"render.month.4": "avril",
"render.month.5": "mai",
"render.month.6": "juin",
"render.month.7": "juillet",
"render.month.8": "août",
"render.month.9": "septembre",
"render.month.10": "octobre",
"render.month.11": "novembre",
"render.month.12": "décembre"
}

View File

@@ -0,0 +1,67 @@
{
"menu.group.file": "Archivio",
"menu.group.edit": "Modifica",
"menu.group.view": "Vista",
"menu.group.blog": "Sezione blog",
"menu.group.help": "Aiuto",
"menu.item.newPost": "Nuovo post",
"menu.item.importMedia": "Importa media...",
"menu.item.save": "Salva",
"menu.item.openInBrowser": "Apri nel browser",
"menu.item.openDataFolder": "Apri cartella dati",
"menu.item.quit": "Esci",
"menu.item.undo": "Annulla",
"menu.item.redo": "Ripeti",
"menu.item.cut": "Taglia",
"menu.item.copy": "Copia",
"menu.item.paste": "Incolla",
"menu.item.delete": "Elimina",
"menu.item.selectAll": "Seleziona tutto",
"menu.item.find": "Trova",
"menu.item.replace": "Sostituisci",
"menu.item.viewPosts": "Post",
"menu.item.viewMedia": "Contenuti media",
"menu.item.toggleSidebar": "Attiva/disattiva barra laterale",
"menu.item.togglePanel": "Attiva/disattiva pannello",
"menu.item.toggleDevTools": "Attiva/disattiva strumenti sviluppatore",
"menu.item.reload": "Ricarica",
"menu.item.forceReload": "Forza ricaricamento",
"menu.item.resetZoom": "Dimensione reale",
"menu.item.zoomIn": "Ingrandisci",
"menu.item.zoomOut": "Riduci",
"menu.item.toggleFullScreen": "Attiva/disattiva schermo intero",
"menu.item.publishSelected": "Pubblica selezionato",
"menu.item.previewPost": "Anteprima post",
"menu.item.rebuildDatabase": "Ricostruisci Database from Files",
"menu.item.reindexText": "Reindex Ricerca Text",
"menu.item.metadataDiff": "Strumento diff metadati",
"menu.item.generateSitemap": "Renderizza sito",
"menu.item.about": "Informazioni su Blogging Desktop Server",
"menu.item.openDocumentation": "Apri documentazione",
"menu.item.viewOnGitHub": "Visualizza su GitHub",
"menu.item.reportIssue": "Segnala problema",
"render.archive": "Archivio",
"render.pagination.label": "Paginazione",
"render.pagination.newer": "più recente",
"render.pagination.older": "più vecchio",
"render.notFound.message": "La pagina di anteprima richiesta non è stata trovata.",
"render.notFound.back": "Torna alla home di anteprima",
"render.photoArchive.empty": "Nessuna foto trovata per questo archivio.",
"render.gallery.empty": "Nessuna immagine collegata trovata.",
"render.tagCloud.empty": "Nessun tag trovato.",
"render.tagCloud.ariaLabel": "Nuvola di tag",
"render.video.youtubeTitle": "Video YouTube",
"render.video.vimeoTitle": "Video Vimeo",
"render.month.1": "gennaio",
"render.month.2": "febbraio",
"render.month.3": "marzo",
"render.month.4": "aprile",
"render.month.5": "maggio",
"render.month.6": "giugno",
"render.month.7": "luglio",
"render.month.8": "agosto",
"render.month.9": "settembre",
"render.month.10": "ottobre",
"render.month.11": "novembre",
"render.month.12": "dicembre"
}

View File

@@ -61,76 +61,76 @@ export const APP_MENU_GROUPS: AppMenuGroupDefinition[] = [
{
label: 'File',
items: [
{ label: 'New Post', action: 'newPost', accelerator: 'CmdOrCtrl+N' },
{ label: 'Import Media...', action: 'importMedia', accelerator: 'CmdOrCtrl+I' },
{ label: 'menu.item.newPost', action: 'newPost', accelerator: 'CmdOrCtrl+N' },
{ label: 'menu.item.importMedia', action: 'importMedia', accelerator: 'CmdOrCtrl+I' },
{ label: '', action: 'file-separator-0', separator: true },
{ label: 'Save', action: 'save', accelerator: 'CmdOrCtrl+S' },
{ label: 'menu.item.save', action: 'save', accelerator: 'CmdOrCtrl+S' },
{ label: '', action: 'file-separator-1', separator: true },
{ label: 'Open in Browser', action: 'openInBrowser' },
{ label: 'menu.item.openInBrowser', action: 'openInBrowser' },
{ label: '', action: 'file-separator-2', separator: true },
{ label: 'Open Data Folder', action: 'openDataFolder' },
{ label: 'menu.item.openDataFolder', action: 'openDataFolder' },
{ label: '', action: 'file-separator-3', separator: true },
{ label: 'Quit', action: 'quit', accelerator: 'CmdOrCtrl+Q' },
{ label: 'menu.item.quit', action: 'quit', accelerator: 'CmdOrCtrl+Q' },
],
},
{
label: 'Edit',
items: [
{ label: 'Undo', action: 'undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: 'Redo', action: 'redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
{ label: 'menu.item.undo', action: 'undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: 'menu.item.redo', action: 'redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
{ label: '', action: 'edit-separator-1', separator: true },
{ label: 'Cut', action: 'cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', action: 'copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'Paste', action: 'paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: 'Delete', action: 'delete', role: 'delete' },
{ label: 'menu.item.cut', action: 'cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'menu.item.copy', action: 'copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'menu.item.paste', action: 'paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: 'menu.item.delete', action: 'delete', role: 'delete' },
{ label: '', action: 'edit-separator-2', separator: true },
{ label: 'Select All', action: 'selectAll', accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
{ label: 'menu.item.selectAll', action: 'selectAll', accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
{ label: '', action: 'edit-separator-3', separator: true },
{ label: 'Find', action: 'find', accelerator: 'CmdOrCtrl+F' },
{ label: 'Replace', action: 'replace', accelerator: 'CmdOrCtrl+H' },
{ label: 'menu.item.find', action: 'find', accelerator: 'CmdOrCtrl+F' },
{ label: 'menu.item.replace', action: 'replace', accelerator: 'CmdOrCtrl+H' },
],
},
{
label: 'View',
items: [
{ label: 'Posts', action: 'viewPosts', accelerator: 'CmdOrCtrl+1' },
{ label: 'Media', action: 'viewMedia', accelerator: 'CmdOrCtrl+2' },
{ label: 'Toggle Sidebar', action: 'toggleSidebar', accelerator: 'CmdOrCtrl+B' },
{ label: 'Toggle Panel', action: 'togglePanel', accelerator: 'CmdOrCtrl+J' },
{ label: 'Toggle Developer Tools', action: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' },
{ label: 'menu.item.viewPosts', action: 'viewPosts', accelerator: 'CmdOrCtrl+1' },
{ label: 'menu.item.viewMedia', action: 'viewMedia', accelerator: 'CmdOrCtrl+2' },
{ label: 'menu.item.toggleSidebar', action: 'toggleSidebar', accelerator: 'CmdOrCtrl+B' },
{ label: 'menu.item.togglePanel', action: 'togglePanel', accelerator: 'CmdOrCtrl+J' },
{ label: 'menu.item.toggleDevTools', action: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' },
{ label: '', action: 'view-separator-1', separator: true },
{ label: 'Reload', action: 'reload' },
{ label: 'Force Reload', action: 'forceReload' },
{ label: 'menu.item.reload', action: 'reload' },
{ label: 'menu.item.forceReload', action: 'forceReload' },
{ label: '', action: 'view-separator-2', separator: true },
{ label: 'Actual Size', action: 'resetZoom' },
{ label: 'Zoom In', action: 'zoomIn' },
{ label: 'Zoom Out', action: 'zoomOut' },
{ label: 'menu.item.resetZoom', action: 'resetZoom' },
{ label: 'menu.item.zoomIn', action: 'zoomIn' },
{ label: 'menu.item.zoomOut', action: 'zoomOut' },
{ label: '', action: 'view-separator-3', separator: true },
{ label: 'Toggle Full Screen', action: 'toggleFullScreen' },
{ label: 'menu.item.toggleFullScreen', action: 'toggleFullScreen' },
],
},
{
label: 'Blog',
items: [
{ label: 'Publish Selected', action: 'publishSelected', accelerator: 'CmdOrCtrl+Shift+P' },
{ label: 'menu.item.publishSelected', action: 'publishSelected', accelerator: 'CmdOrCtrl+Shift+P' },
{ label: '', action: 'blog-separator-1', separator: true },
{ label: 'Preview Post', action: 'previewPost', id: APP_MENU_ITEM_IDS.previewPost, enabled: false, accelerator: 'CmdOrCtrl+Shift+V' },
{ label: 'menu.item.previewPost', action: 'previewPost', id: APP_MENU_ITEM_IDS.previewPost, enabled: false, accelerator: 'CmdOrCtrl+Shift+V' },
{ label: '', action: 'blog-separator-2', separator: true },
{ label: 'Rebuild Database from Files', action: 'rebuildDatabase' },
{ label: 'Reindex Search Text', action: 'reindexText' },
{ label: 'menu.item.rebuildDatabase', action: 'rebuildDatabase' },
{ label: 'menu.item.reindexText', action: 'reindexText' },
{ label: '', action: 'blog-separator-3', separator: true },
{ label: 'Metadata Diff Tool', action: 'metadataDiff' },
{ label: 'Render Site', action: 'generateSitemap', accelerator: 'CmdOrCtrl+R' },
{ label: 'menu.item.metadataDiff', action: 'metadataDiff' },
{ label: 'menu.item.generateSitemap', action: 'generateSitemap', accelerator: 'CmdOrCtrl+R' },
],
},
{
label: 'Help',
items: [
{ label: 'About Blogging Desktop Server', action: 'about' },
{ label: 'Open Documentation', action: 'openDocumentation' },
{ label: 'menu.item.about', action: 'about' },
{ label: 'menu.item.openDocumentation', action: 'openDocumentation' },
{ label: '', action: 'help-separator-1', separator: true },
{ label: 'View on GitHub', action: 'viewOnGitHub' },
{ label: 'Report Issue', action: 'reportIssue' },
{ label: 'menu.item.viewOnGitHub', action: 'viewOnGitHub' },
{ label: 'menu.item.reportIssue', action: 'reportIssue' },
],
},
];