Compare commits
4 Commits
d6be9beebf
...
v1.0.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aa741ab0a | |||
| f69952a5da | |||
| 81af9bf3dd | |||
| f1b617e967 |
@@ -168,15 +168,6 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: 📦 Restore npm cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -427,7 +418,6 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -533,7 +523,13 @@ jobs:
|
|||||||
-F "priority=4" || true
|
-F "priority=4" || true
|
||||||
|
|
||||||
- name: 🔔 Gotify - Failure
|
- name: 🔔 Gotify - Failure
|
||||||
if: needs.deploy.result == 'failure' || needs.build.result == 'failure' || needs.qa.result == 'failure'
|
if: |
|
||||||
|
needs.prepare.result == 'failure' ||
|
||||||
|
needs.qa.result == 'failure' ||
|
||||||
|
needs.build-app.result == 'failure' ||
|
||||||
|
needs.build-gatekeeper.result == 'failure' ||
|
||||||
|
needs.deploy.result == 'failure' ||
|
||||||
|
needs.pagespeed.result == 'failure'
|
||||||
run: |
|
run: |
|
||||||
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
|
||||||
-F "title=❌ Deployment FEHLGESCHLAGEN – ${{ needs.prepare.outputs.target || 'unknown' }}" \
|
-F "title=❌ Deployment FEHLGESCHLAGEN – ${{ needs.prepare.outputs.target || 'unknown' }}" \
|
||||||
|
|||||||
@@ -69,9 +69,19 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
logger.info('Contact form email sent successfully', { messageId: result.messageId });
|
logger.info('Contact form email sent successfully', { messageId: result.messageId });
|
||||||
|
await services.notifications.notify({
|
||||||
|
title: `📩 ${subject}`,
|
||||||
|
message: `New message from ${name} (${email}):\n\n${message}`,
|
||||||
|
priority: 5,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to send contact form email', { error: result.error });
|
logger.error('Failed to send contact form email', { error: result.error });
|
||||||
services.errors.captureException(result.error, { action: 'sendContactFormAction', email });
|
services.errors.captureException(result.error, { action: 'sendContactFormAction', email });
|
||||||
|
await services.notifications.notify({
|
||||||
|
title: '🚨 Contact Form Error',
|
||||||
|
message: `Failed to send email for ${name} (${email}). Error: ${JSON.stringify(result.error)}`,
|
||||||
|
priority: 8,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ function createConfig() {
|
|||||||
internalUrl: env.INTERNAL_DIRECTUS_URL,
|
internalUrl: env.INTERNAL_DIRECTUS_URL,
|
||||||
proxyPath: '/cms',
|
proxyPath: '/cms',
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
gotify: {
|
||||||
|
url: env.GOTIFY_URL,
|
||||||
|
token: env.GOTIFY_TOKEN,
|
||||||
|
enabled: Boolean(env.GOTIFY_URL && env.GOTIFY_TOKEN),
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +134,9 @@ export const config = {
|
|||||||
get directus() {
|
get directus() {
|
||||||
return getConfig().directus;
|
return getConfig().directus;
|
||||||
},
|
},
|
||||||
|
get notifications() {
|
||||||
|
return getConfig().notifications;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,5 +181,12 @@ export function getMaskedConfig() {
|
|||||||
password: mask(c.directus.password),
|
password: mask(c.directus.password),
|
||||||
token: mask(c.directus.token),
|
token: mask(c.directus.token),
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
gotify: {
|
||||||
|
url: c.notifications.gotify.url,
|
||||||
|
token: mask(c.notifications.gotify.token),
|
||||||
|
enabled: c.notifications.gotify.enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export const envSchema = z.object({
|
|||||||
|
|
||||||
// Deploy Target
|
// Deploy Target
|
||||||
TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(),
|
TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(),
|
||||||
|
// Gotify
|
||||||
|
GOTIFY_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()),
|
||||||
|
GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Env = z.infer<typeof envSchema>;
|
export type Env = z.infer<typeof envSchema>;
|
||||||
@@ -78,5 +81,7 @@ export function getRawEnv() {
|
|||||||
DIRECTUS_API_TOKEN: process.env.DIRECTUS_API_TOKEN,
|
DIRECTUS_API_TOKEN: process.env.DIRECTUS_API_TOKEN,
|
||||||
INTERNAL_DIRECTUS_URL: process.env.INTERNAL_DIRECTUS_URL,
|
INTERNAL_DIRECTUS_URL: process.env.INTERNAL_DIRECTUS_URL,
|
||||||
TARGET: process.env.TARGET,
|
TARGET: process.env.TARGET,
|
||||||
|
GOTIFY_URL: process.env.GOTIFY_URL,
|
||||||
|
GOTIFY_TOKEN: process.env.GOTIFY_TOKEN,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { AnalyticsService } from './analytics/analytics-service';
|
|||||||
import type { CacheService } from './cache/cache-service';
|
import type { CacheService } from './cache/cache-service';
|
||||||
import type { ErrorReportingService } from './errors/error-reporting-service';
|
import type { ErrorReportingService } from './errors/error-reporting-service';
|
||||||
import type { LoggerService } from './logging/logger-service';
|
import type { LoggerService } from './logging/logger-service';
|
||||||
|
import type { NotificationService } from './notifications/notification-service';
|
||||||
|
|
||||||
// Simple constructor-based DI container.
|
// Simple constructor-based DI container.
|
||||||
export class AppServices {
|
export class AppServices {
|
||||||
@@ -9,6 +10,7 @@ export class AppServices {
|
|||||||
public readonly analytics: AnalyticsService,
|
public readonly analytics: AnalyticsService,
|
||||||
public readonly errors: ErrorReportingService,
|
public readonly errors: ErrorReportingService,
|
||||||
public readonly cache: CacheService,
|
public readonly cache: CacheService,
|
||||||
public readonly logger: LoggerService
|
public readonly logger: LoggerService,
|
||||||
|
public readonly notifications: NotificationService,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { UmamiAnalyticsService } from './analytics/umami-analytics-service';
|
|||||||
import { MemoryCacheService } from './cache/memory-cache-service';
|
import { MemoryCacheService } from './cache/memory-cache-service';
|
||||||
import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service';
|
import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service';
|
||||||
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
|
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
|
||||||
|
import {
|
||||||
|
GotifyNotificationService,
|
||||||
|
NoopNotificationService,
|
||||||
|
} from './notifications/gotify-notification-service';
|
||||||
import { PinoLoggerService } from './logging/pino-logger-service';
|
import { PinoLoggerService } from './logging/pino-logger-service';
|
||||||
import { config, getMaskedConfig } from '../config';
|
import { config, getMaskedConfig } from '../config';
|
||||||
|
|
||||||
@@ -23,6 +27,7 @@ export function getServerAppServices(): AppServices {
|
|||||||
umamiEnabled: config.analytics.umami.enabled,
|
umamiEnabled: config.analytics.umami.enabled,
|
||||||
sentryEnabled: config.errors.glitchtip.enabled,
|
sentryEnabled: config.errors.glitchtip.enabled,
|
||||||
mailEnabled: Boolean(config.mail.host && config.mail.user),
|
mailEnabled: Boolean(config.mail.host && config.mail.user),
|
||||||
|
gotifyEnabled: config.notifications.gotify.enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const analytics = config.analytics.umami.enabled
|
const analytics = config.analytics.umami.enabled
|
||||||
@@ -35,8 +40,22 @@ export function getServerAppServices(): AppServices {
|
|||||||
logger.info('Noop analytics service initialized (analytics disabled)');
|
logger.info('Noop analytics service initialized (analytics disabled)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifications = config.notifications.gotify.enabled
|
||||||
|
? new GotifyNotificationService({
|
||||||
|
url: config.notifications.gotify.url!,
|
||||||
|
token: config.notifications.gotify.token!,
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
: new NoopNotificationService();
|
||||||
|
|
||||||
|
if (config.notifications.gotify.enabled) {
|
||||||
|
logger.info('Gotify notification service initialized');
|
||||||
|
} else {
|
||||||
|
logger.info('Noop notification service initialized (notifications disabled)');
|
||||||
|
}
|
||||||
|
|
||||||
const errors = config.errors.glitchtip.enabled
|
const errors = config.errors.glitchtip.enabled
|
||||||
? new GlitchtipErrorReportingService({ enabled: true })
|
? new GlitchtipErrorReportingService({ enabled: true }, notifications)
|
||||||
: new NoopErrorReportingService();
|
: new NoopErrorReportingService();
|
||||||
|
|
||||||
if (config.errors.glitchtip.enabled) {
|
if (config.errors.glitchtip.enabled) {
|
||||||
@@ -55,7 +74,7 @@ export function getServerAppServices(): AppServices {
|
|||||||
level: config.logging.level,
|
level: config.logging.level,
|
||||||
});
|
});
|
||||||
|
|
||||||
singleton = new AppServices(analytics, errors, cache, logger);
|
singleton = new AppServices(analytics, errors, cache, logger, notifications);
|
||||||
|
|
||||||
logger.info('All application services initialized successfully');
|
logger.info('All application services initialized successfully');
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporti
|
|||||||
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
|
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
|
||||||
import { NoopLoggerService } from './logging/noop-logger-service';
|
import { NoopLoggerService } from './logging/noop-logger-service';
|
||||||
import { PinoLoggerService } from './logging/pino-logger-service';
|
import { PinoLoggerService } from './logging/pino-logger-service';
|
||||||
|
import { NoopNotificationService } from './notifications/gotify-notification-service';
|
||||||
import { config, getMaskedConfig } from '../config';
|
import { config, getMaskedConfig } from '../config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,9 +72,7 @@ export function getAppServices(): AppServices {
|
|||||||
|
|
||||||
// Create logger first to log initialization
|
// Create logger first to log initialization
|
||||||
const logger =
|
const logger =
|
||||||
typeof window === 'undefined'
|
typeof window === 'undefined' ? new PinoLoggerService('server') : new NoopLoggerService();
|
||||||
? new PinoLoggerService('server')
|
|
||||||
: new NoopLoggerService();
|
|
||||||
|
|
||||||
// Log initialization
|
// Log initialization
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -121,7 +120,9 @@ export function getAppServices(): AppServices {
|
|||||||
: new NoopErrorReportingService();
|
: new NoopErrorReportingService();
|
||||||
|
|
||||||
if (sentryEnabled) {
|
if (sentryEnabled) {
|
||||||
logger.info(`GlitchTip error reporting service initialized (${typeof window === 'undefined' ? 'server' : 'client'})`);
|
logger.info(
|
||||||
|
`GlitchTip error reporting service initialized (${typeof window === 'undefined' ? 'server' : 'client'})`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.info('Noop error reporting service initialized (error reporting disabled)');
|
logger.info('Noop error reporting service initialized (error reporting disabled)');
|
||||||
}
|
}
|
||||||
@@ -138,9 +139,10 @@ export function getAppServices(): AppServices {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create and cache the singleton
|
// Create and cache the singleton
|
||||||
singleton = new AppServices(analytics, errors, cache, logger);
|
const notifications = new NoopNotificationService();
|
||||||
|
singleton = new AppServices(analytics, errors, cache, logger, notifications);
|
||||||
|
|
||||||
logger.info('All application services initialized successfully');
|
logger.info('All application services initialized successfully');
|
||||||
|
|
||||||
return singleton;
|
return singleton;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,15 @@ export type ErrorReportingUser = {
|
|||||||
export type ErrorReportingLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'log';
|
export type ErrorReportingLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'log';
|
||||||
|
|
||||||
export interface ErrorReportingService {
|
export interface ErrorReportingService {
|
||||||
captureException(error: unknown, context?: Record<string, unknown>): string | undefined;
|
captureException(
|
||||||
captureMessage(message: string, level?: ErrorReportingLevel): string | undefined;
|
error: unknown,
|
||||||
|
context?: Record<string, unknown>,
|
||||||
|
): Promise<string | undefined> | string | undefined;
|
||||||
|
captureMessage(
|
||||||
|
message: string,
|
||||||
|
level?: ErrorReportingLevel,
|
||||||
|
): Promise<string | undefined> | string | undefined;
|
||||||
setUser(user: ErrorReportingUser | null): void;
|
setUser(user: ErrorReportingUser | null): void;
|
||||||
setTag(key: string, value: string): void;
|
setTag(key: string, value: string): void;
|
||||||
withScope<T>(fn: () => T, context?: Record<string, unknown>): T;
|
withScope<T>(fn: () => T, context?: Record<string, unknown>): T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
ErrorReportingService,
|
ErrorReportingService,
|
||||||
ErrorReportingUser,
|
ErrorReportingUser,
|
||||||
} from './error-reporting-service';
|
} from './error-reporting-service';
|
||||||
|
import type { NotificationService } from '../notifications/notification-service';
|
||||||
|
|
||||||
type SentryLike = typeof Sentry;
|
type SentryLike = typeof Sentry;
|
||||||
|
|
||||||
@@ -15,12 +16,29 @@ export type GlitchtipErrorReportingServiceOptions = {
|
|||||||
export class GlitchtipErrorReportingService implements ErrorReportingService {
|
export class GlitchtipErrorReportingService implements ErrorReportingService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly options: GlitchtipErrorReportingServiceOptions,
|
private readonly options: GlitchtipErrorReportingServiceOptions,
|
||||||
private readonly sentry: SentryLike = Sentry
|
private readonly notifications?: NotificationService,
|
||||||
|
private readonly sentry: SentryLike = Sentry,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
captureException(error: unknown, context?: Record<string, unknown>) {
|
async captureException(error: unknown, context?: Record<string, unknown>) {
|
||||||
if (!this.options.enabled) return undefined;
|
if (!this.options.enabled) return undefined;
|
||||||
return this.sentry.captureException(error, context as any) as any;
|
const result = this.sentry.captureException(error, context as any) as any;
|
||||||
|
|
||||||
|
// Send to Gotify if it's considered critical or if we just want all exceptions there
|
||||||
|
// For now, let's send all exceptions to Gotify as requested "notify me via gotify about critical error messages"
|
||||||
|
// We'll treat all captureException calls as potentially critical or at least noteworthy
|
||||||
|
if (this.notifications) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
|
||||||
|
|
||||||
|
await this.notifications.notify({
|
||||||
|
title: '🔥 Critical Error Captured',
|
||||||
|
message: `Error: ${errorMessage}${contextStr}`,
|
||||||
|
priority: 7,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
captureMessage(message: string, level: ErrorReportingLevel = 'error') {
|
captureMessage(message: string, level: ErrorReportingLevel = 'error') {
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import type { ErrorReportingLevel, ErrorReportingService, ErrorReportingUser } from './error-reporting-service';
|
import type {
|
||||||
|
ErrorReportingLevel,
|
||||||
|
ErrorReportingService,
|
||||||
|
ErrorReportingUser,
|
||||||
|
} from './error-reporting-service';
|
||||||
|
|
||||||
export class NoopErrorReportingService implements ErrorReportingService {
|
export class NoopErrorReportingService implements ErrorReportingService {
|
||||||
captureException(_error: unknown, _context?: Record<string, unknown>) {
|
async captureException(_error: unknown, _context?: Record<string, unknown>) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
captureMessage(_message: string, _level?: ErrorReportingLevel) {
|
async captureMessage(_message: string, _level?: ErrorReportingLevel) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,19 @@ export class PinoLoggerService implements LoggerService {
|
|||||||
// In Next.js, especially in the Edge runtime or during instrumentation,
|
// In Next.js, especially in the Edge runtime or during instrumentation,
|
||||||
// pino transports (which use worker threads) can cause issues.
|
// pino transports (which use worker threads) can cause issues.
|
||||||
// We disable transport in production and during instrumentation.
|
// We disable transport in production and during instrumentation.
|
||||||
const useTransport = !config.isProduction && typeof window === 'undefined';
|
const useTransport = config.isDevelopment && typeof window === 'undefined';
|
||||||
|
|
||||||
this.logger = pino({
|
this.logger = pino({
|
||||||
name: name || 'app',
|
name: name || 'app',
|
||||||
level: config.logging.level,
|
level: config.logging.level,
|
||||||
transport:
|
transport: useTransport
|
||||||
useTransport
|
? {
|
||||||
? {
|
target: 'pino-pretty',
|
||||||
target: 'pino-pretty',
|
options: {
|
||||||
options: {
|
colorize: true,
|
||||||
colorize: true,
|
},
|
||||||
},
|
}
|
||||||
}
|
: undefined,
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
lib/services/notifications/gotify-notification-service.ts
Normal file
49
lib/services/notifications/gotify-notification-service.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { NotificationOptions, NotificationService } from './notification-service';
|
||||||
|
|
||||||
|
export interface GotifyConfig {
|
||||||
|
url: string;
|
||||||
|
token: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GotifyNotificationService implements NotificationService {
|
||||||
|
constructor(private config: GotifyConfig) {}
|
||||||
|
|
||||||
|
async notify(options: NotificationOptions): Promise<void> {
|
||||||
|
if (!this.config.enabled) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { title, message, priority = 4 } = options;
|
||||||
|
const url = new URL('message', this.config.url);
|
||||||
|
url.searchParams.set('token', this.config.token);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
priority,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Gotify notification failed:', {
|
||||||
|
status: response.status,
|
||||||
|
error: errorText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gotify notification error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NoopNotificationService implements NotificationService {
|
||||||
|
async notify(): Promise<void> {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/services/notifications/notification-service.ts
Normal file
9
lib/services/notifications/notification-service.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface NotificationOptions {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationService {
|
||||||
|
notify(options: NotificationOptions): Promise<void>;
|
||||||
|
}
|
||||||
@@ -191,7 +191,14 @@
|
|||||||
"emailPlaceholder": "ihre@email.de",
|
"emailPlaceholder": "ihre@email.de",
|
||||||
"message": "Nachricht",
|
"message": "Nachricht",
|
||||||
"messagePlaceholder": "Wie können wir Ihnen helfen?",
|
"messagePlaceholder": "Wie können wir Ihnen helfen?",
|
||||||
"submit": "Nachricht senden"
|
"submit": "Nachricht senden",
|
||||||
|
"submitting": "Wird gesendet...",
|
||||||
|
"successTitle": "Nachricht gesendet!",
|
||||||
|
"successDesc": "Vielen Dank für Ihre Nachricht. Wir werden uns so schnell wie möglich bei Ihnen melden.",
|
||||||
|
"sendAnother": "Weitere Nachricht senden",
|
||||||
|
"errorTitle": "Senden fehlgeschlagen!",
|
||||||
|
"error": "Etwas ist schief gelaufen. Bitte überprüfen Sie Ihre Eingaben und versuchen Sie es erneut.",
|
||||||
|
"tryAgain": "Erneut versuchen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Products": {
|
"Products": {
|
||||||
|
|||||||
@@ -191,7 +191,14 @@
|
|||||||
"emailPlaceholder": "your@email.com",
|
"emailPlaceholder": "your@email.com",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
"messagePlaceholder": "How can we help you?",
|
"messagePlaceholder": "How can we help you?",
|
||||||
"submit": "Send Message"
|
"submit": "Send Message",
|
||||||
|
"submitting": "Sending...",
|
||||||
|
"successTitle": "Message Sent!",
|
||||||
|
"successDesc": "Thank you for your message. We will get back to you as soon as possible.",
|
||||||
|
"sendAnother": "Send another message",
|
||||||
|
"errorTitle": "Submission Failed!",
|
||||||
|
"error": "Something went wrong. Please check your input and try again.",
|
||||||
|
"tryAgain": "Try Again"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Products": {
|
"Products": {
|
||||||
|
|||||||
Reference in New Issue
Block a user