Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion context/TranslationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import fr from '@/locales/fr.json';
import zh from '@/locales/zh.json';
import ja from '@/locales/ja.json';
import ko from '@/locales/ko.json';
import de from '@/locales/de.json';

export type Language = 'en' | 'es' | 'hi' | 'fr' | 'zh' | 'ja' | 'ko';
export type Language = 'en' | 'es' | 'hi' | 'fr' | 'zh' | 'ja' | 'ko' | 'de';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const translations: Record<Language, any> = {
Expand All @@ -28,6 +29,7 @@ const translations: Record<Language, any> = {
zh,
ja,
ko,
de,
};

export const LANGUAGE_LABELS: Record<Language, string> = {
Expand All @@ -38,6 +40,7 @@ export const LANGUAGE_LABELS: Record<Language, string> = {
zh: '\u7b80\u4f53\u4e2d\u6587',
ja: '\u65e5\u672c\u8a9e',
ko: '\ud55c\uad6d\uc5b4',
de: 'Deutsch',
};

interface TranslationContextType {
Expand Down
254 changes: 254 additions & 0 deletions locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
{
"navbar": {
"home": "Zur Startseite",
"repo": "GitHub-Repo",
"contributors": "Mitwirkende",
"theme_toggle": "Theme umschalten",
"menu_open": "Menü öffnen",
"menu_close": "Menü schließen",
"compare": "Vergleichen",
"customization_studio": "Anpassungsstudio",
"generator": "Generator",
"burnout_radar": "Burnout-Radar"
},
"footer": {
"tagline": "Entwickelt für die Elite-Builder-Community.",
"contributors": "Mitwirkende",
"documentation": "Dokumentation",
"creator": "Ersteller",
"copyright": "© {{year}} CommitPulse. Alle Rechte vorbehalten.",
"navigation": "Navigation",
"resources": "Ressourcen",
"connect": "Verbinden",
"made_with": "Mit ❤️ für Entwickler erstellt",
"home": "Startseite",
"compare": "Vergleichen",
"customization": "Anpassung",
"github_repo": "GitHub-Repository",
"github": "GitHub",
"creator_github": "Ersteller auf GitHub",
"discord": "Discord",
"twitter": "Twitter",
"linkedin": "LinkedIn",
"generator": "Generator"
},
"landing": {
"empty_username_warning": "Bitte gib einen GitHub-Benutzernamen ein, um deinen Badge-Link zu kopieren.",
"max_length_warning": "GitHub-Benutzername-Limit erreicht (maximal 39 Zeichen)",
"title": "Werte deine\n{Contribution}-Story auf.",
"subtitle": "Gib dich nicht mehr mit flachen Rastern zufrieden. Erzeuge hochauflösende, isometrische 3D-Monolithen, die deinen Coding-Rhythmus mit professioneller Präzision visualisieren.",
"watch_dashboard": "Dashboard ansehen",
"discord_community": "Tritt der Core-Community auf Discord bei",
"input_placeholder": "GitHub-Benutzernamen eingeben",
"clear_input": "Eingabe löschen",
"copy_link": "Link kopieren",
"copied": "Kopiert",
"recent": "Kürzlich:",
"clear": "Löschen",
"preview_placeholder_title": "Bereit, deinen Rhythmus zu visualisieren?",
"preview_placeholder_desc": "Gib oben einen GitHub-Benutzernamen ein, um sofort dein Streak-Badge zu erzeugen.",
"user_not_found": "GitHub-Benutzer nicht gefunden",
"user_not_found_desc": "Bitte überprüfe den Benutzernamen und versuche es erneut.",
"preview_auto_theme": "Zufälliges Theme ändert sich bei jedem Seitenaufruf und deaktiviert das Caching",
"preview_caching_tip": "Vorschau aktualisiert sich bei jeder Änderung. Das gehostete Badge wird um UTC-Mitternacht zwischengespeichert",
"preview_empty_tip": "Füge einen Benutzernamen hinzu, um Live-Vorschau und Export-Snippets zu aktivieren",
"features": {
"sync_title": "Echtzeit-Sync",
"sync_desc": "Direkt von der GitHub-GraphQL-API abgerufen. Dein Streak aktualisiert sich so schnell, wie du Code pushst.",
"theme_title": "Theme-Engine",
"theme_desc": "Wechsle zwischen Neon, Dracula oder benutzerdefinierten HEX-Modi über einfache URL-Verwaltung.",
"isometric_title": "Isometrische Mathematik",
"isometric_desc": "Ausgefeilte 3D-Projektionsformeln verwandeln 2D-Daten in digitale Architektur."
},
"generate_badge": "Badge erzeugen",
"verified_profile": "Verifiziertes Profil",
"verifying": "Wird verifiziert ...",
"preview_monolith": "MONOLITH-VORSCHAU",
"interactive_preview_title": "Interaktive Monolith-Vorschau",
"interactive_preview_desc": "CommitPulse fasst deinen öffentlichen GitHub-Beitragsverlauf zu einer anpassbaren 3D-Stadt zusammen. Je höher die Türme, desto mehr hast du an diesem Tag committet. Gib oben einen GitHub-Benutzernamen ein, um sofort dein Streak-Badge zu erzeugen.",
"input_aria_label": "GitHub-Benutzernamen eingeben, um ein Badge zu erzeugen",
"unable_to_load_stats": "Statistiken konnten nicht geladen werden"
},
"success_guide": {
"title": "Dein Monolith ist bereit – setze ihn in 4 Schritten ein",
"markdown_copied": "Markdown kopiert",
"dismiss_aria": "Anleitung schließen",
"step_1_title": "Öffne dein Profil-Repo",
"step_1_body": "Gehe zu github.com/DEIN_BENUTZERNAME/DEIN_BENUTZERNAME – deinem speziellen Profil-Repository.",
"step_2_title": "README.md bearbeiten",
"step_2_body": "Klicke auf das Stift-Symbol, um die Datei im integrierten Editor von GitHub zu öffnen.",
"step_3_title": "Snippet einfügen",
"step_3_body": "Setze den Cursor an die Stelle, an der der Monolith erscheinen soll, und füge ihn dann ein (Strg+V / Cmd+V).",
"step_4_title": "Speichern & veröffentlichen",
"step_4_body": "Klicke auf „Commit changes“ und besuche dein Profil. Dein 3D-Streak ist jetzt live.",
"copied_snippet_label": "Dein kopiertes Snippet",
"color_tip": "Tipp: Füge der URL ?accent=808080 hinzu, um die Farbpalette deines Monolithen zu ändern.",
"watch_dashboard_btn": "Dein Dashboard ansehen",
"customize_more_btn": "Möchtest du mehr anpassen?"
},
"customize_cta": {
"studio_badge": "Anpassungsstudio",
"title": "Möchtest du deinen Monolithen feinabstimmen?",
"desc": "Stelle jedes Pixel genau ein – tausche Akzentfarben, probiere ein dunkles oder Neon-Theme, schalte die logarithmische Höhenskalierung um und sieh dir Änderungen live an, bevor du eine einzige Zeile einfügst.",
"btn": "Anpassungsstudio öffnen"
},
"customize": {
"back_to_home": "Zurück zur Startseite",
"title": "Stimme deinen Monolithen fein ab.",
"desc": "Jede Änderung unten aktualisiert die Vorschau in Echtzeit. Kopiere das Export-Snippet, wenn du fertig bist. Keine weiteren Schritte nötig.",
"live_preview": "Live-Vorschau",
"active_params": "Aktive Parameter",
"empty_preview_desc": "Die Live-Badge-Vorschau erscheint hier, sobald ein Benutzername hinzugefügt wurde.",
"controls": {
"username": "GitHub-Benutzername",
"username_placeholder": "Benutzernamen eingeben ...",
"select_theme": "Theme auswählen",
"theme_placeholder": "Voreingestelltes Theme auswählen",
"theme_mode": "Theme-Modus",
"custom_bg": "Benutzerdefinierter Hintergrund",
"custom_accent": "Benutzerdefinierter Akzent",
"custom_text": "Benutzerdefinierter Text",
"log_scaling": "Logarithmische Höhenskalierung",
"speed": "Animationsgeschwindigkeit",
"radius": "Eckenradius",
"badge_size": "Badge-Größe",
"sync_year": "Sync-Jahr",
"clear_custom": "Benutzerdefinierte Farben zurücksetzen",
"theme_presets": "Theme-Voreinstellung",
"color_overrides": "Benutzerdefinierte Farbüberschreibungen",
"font": "Schriftart",
"custom_font_option": "Benutzerdefinierte Google-Schriftart ...",
"custom_font_placeholder": "z. B. Orbitron, Space Mono, Inter"
},
"export": {
"snippet_title": "Export-Snippet",
"snippet_desc": "Wechsle Formate, ohne die Live-Badge-Konfiguration zu ändern.",
"markdown": "Markdown",
"html": "HTML",
"action": "GitHub Action",
"download_badge": "Badge herunterladen",
"downloading": "Wird heruntergeladen ...",
"download_not_available": "Download nicht verfügbar",
"copy_workflow": "Workflow kopieren",
"copy_format": "{{format}} kopieren",
"copied": "Kopiert!",
"copy_aria_disabled": "Füge einen GitHub-Benutzernamen hinzu, um das Kopieren des {{format}}-Export-Snippets zu aktivieren",
"copy_aria_enabled": "{{format}}-Export-Snippet in die Zwischenablage kopieren",
"footer_tip": "Füge dies in die README.md deines GitHub-Profils ein. Das Badge wird serverseitig gerendert, kein Skript erforderlich.",
"tsx": "React TSX",
"download_svg": "SVG herunterladen",
"download_png": "PNG herunterladen"
}
},
"dashboard": {
"generate_btn": "Erstelle dein eigenes Dashboard",
"refresh_btn": "Daten aktualisieren",
"refreshing": "Wird aktualisiert ...",
"refreshed_toast": "Dashboard erfolgreich aktualisiert",
"profile": {
"score": "Entwickler-Score",
"repos": "Repositories",
"followers": "Follower",
"following": "Folgt",
"stars": "Sterne",
"joined": "Beigetreten",
"pro_badge": "PRO",
"pro": "PRO",
"share": "Teile deinen Pulse"
},
"achievements": {
"title": "Erfolge",
"see_all": "Alle Erfolge ansehen",
"show_less": "Weniger anzeigen"
},
"activity": {
"title": "Aktivitätslandschaft",
"year": "Jahr",
"month": "Monat",
"week": "Woche",
"commits": "Commits",
"intensity": "Commit-Intensität",
"loc": "Codezeilen",
"loc_desc": "Im Zeitverlauf geänderte Codezeilen",
"commits_desc": "Commit-Häufigkeit im Zeitverlauf",
"lines_modified": "{{count}} Zeilen geändert",
"aria_range": "von {{start}} bis {{end}}",
"aria_single": "am {{date}}"
},
"languages": {
"title": "Top-Sprachen",
"primary": "Hauptsprache",
"no_data": "Keine Sprachdaten gefunden"
},
"clock": {
"title": "Commit-Uhr",
"active_days": "Wöchentlicher Aktivitätszyklus",
"tooltip_no_commits": "Keine Commits für diesen Wochentag erfasst",
"tooltip_peak": "Aktivster Wochentag in diesem Zyklus",
"tooltip_activity": "Wöchentlicher Aktivitätspunkt"
},
"heatmap": {
"title": "Beitrags-Heatmap",
"less": "Weniger",
"more": "Mehr",
"last_365": "Letzte 365 Tage",
"tooltip_single": "{{count}} Beitrag am {{date}}",
"tooltip_plural": "{{count}} Beiträge am {{date}}",
"no_activity": "Keine Aktivität erfasst",
"peak_activity": "Aktivster Tag",
"high_activity": "Tag mit hoher Aktivität",
"steady_contribution": "Tag mit gleichmäßigen Beiträgen",
"light_activity": "Tag mit geringer Aktivität",
"no_active_streak": "Keine aktive Serie",
"active_streak": "{{streak}}-tägige aktive Serie",
"empty": "Keine aktuelle Aktivität zum Anzeigen",
"code_activity": "Code-Aktivität erfasst",
"no_code_changes": "Keine Code-Änderungen erfasst"
},
"insights": {
"title": "KI-Einblicke"
},
"stats": {
"current_streak": "Aktuelle Serie",
"peak_streak": "Höchste Serie",
"contributions": "Beiträge",
"days": "Tage",
"last_year": "Letztes Jahr",
"utc_disclaimer": "Serie in UTC-Zeitzone berechnet"
},
"share": {
"title": "Pulse teilen",
"close_aria": "Teilen-Panel schließen",
"copy_link": "Link kopieren",
"copy_link_desc": "Profil-URL in die Zwischenablage kopieren",
"link_copied": "Link kopiert",
"share_x": "Auf X teilen",
"share_x_desc": "Teile deinen Pulse mit der Welt",
"share_linkedin": "LinkedIn",
"share_linkedin_desc": "Teile deine Entwickler-Aktivität mit deinem Netzwerk",
"copy_markdown": "README-Markdown kopieren",
"copy_markdown_desc": "Markdown-Snippet für deine README kopieren",
"download_png": "PNG-Snapshot herunterladen",
"download_png_desc": "Speichere einen Snapshot deines Dashboards",
"download_svg": "Vektor-SVG-Monolith herunterladen",
"download_svg_desc": "Lade das rohe Monolith-SVG herunter",
"download_json": "Strukturierte JSON-Daten exportieren",
"download_json_desc": "Exportiere rohe Streak- und Sprachdaten",
"share_os": "System-Teilen",
"share_os_desc": "AirDrop, WhatsApp, Messages & mehr",
"share_os_fallback": "Weitere Optionen",
"share_os_fallback_desc": "Öffne den System-Teilen-Dialog",
"share_reddit": "Reddit",
"share_reddit_desc": "Auf Reddit teilen",
"downloaded": "Asset gespeichert!",
"svg_downloaded": "SVG heruntergeladen!",
"json_downloaded": "JSON heruntergeladen!",
"failed": "Fehlgeschlagen – versuche es erneut",
"social_channels": "Social-Media-Kanäle",
"export_options": "Export-Optionen",
"github_wrapped": "GitHub Wrapped",
"download_webp": "Optimiertes WebP herunterladen",
"download_stl": "Druckbares 3D-STL herunterladen (Demnächst)"
}
}
}
59 changes: 59 additions & 0 deletions locales/de.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, it, expect } from 'vitest';
import { LANGUAGE_LABELS } from '@/context/TranslationContext';
import en from './en.json';
import de from './de.json';

// Flatten a nested locale object into dot-paths -> string leaves. We compare the
// *entire* key surface (not just the top level) because a single missing nested
// key — e.g. one dashboard.share.* entry — silently falls back to English in the
// UI via the t() helper, which is exactly the kind of gap a reviewer can't spot
// by eye across 200+ strings.
function flatten(obj: Record<string, unknown>, prefix = ''): Record<string, string> {
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (value !== null && typeof value === 'object') {
Object.assign(out, flatten(value as Record<string, unknown>, path));
} else {
out[path] = String(value);
}
}
return out;
}

// Collect the {{placeholder}} tokens a string exposes to t()'s param replacement,
// sorted so order differences between languages don't cause false failures.
function placeholders(value: string): string[] {
return (value.match(/{{\s*\w+\s*}}/g) ?? []).sort();
}

const enFlat = flatten(en);
const deFlat = flatten(de);

describe('German (de) locale', () => {
it('is registered in the language switcher as Deutsch', () => {
expect(LANGUAGE_LABELS.de).toBe('Deutsch');
});

it('defines exactly the same keys as the English source of truth', () => {
// Sorted comparison surfaces both missing keys (dropped translations) and
// extra keys (typos/renames that would never be read) in one assertion.
expect(Object.keys(deFlat).sort()).toEqual(Object.keys(enFlat).sort());
});

it('has a non-empty translation for every key', () => {
const blank = Object.entries(deFlat)
.filter(([, value]) => value.trim().length === 0)
.map(([path]) => path);
expect(blank).toEqual([]);
});

it('preserves the same {{placeholder}} tokens as English in every value', () => {
// A streak count or year that loses its {{token}} in translation renders a
// literal "{{count}}" to the user, so token parity is checked per key.
const mismatched = Object.keys(enFlat).filter(
(path) => placeholders(enFlat[path]).join(',') !== placeholders(deFlat[path] ?? '').join(',')
);
expect(mismatched).toEqual([]);
});
});
Loading