725 lines
17 KiB
TypeScript
725 lines
17 KiB
TypeScript
import { customerToneGuidelines } from "./customer-tone-guidelines";
|
|
|
|
const GERMAN_MARKERS = new Set([
|
|
"ich",
|
|
"mich",
|
|
"mir",
|
|
"mein",
|
|
"meine",
|
|
"wir",
|
|
"du",
|
|
"sie",
|
|
"er",
|
|
"sie",
|
|
"der",
|
|
"die",
|
|
"das",
|
|
"und",
|
|
"ist",
|
|
"sind",
|
|
"sind",
|
|
"waren",
|
|
"hat",
|
|
"habe",
|
|
"haben",
|
|
"eine",
|
|
"einer",
|
|
"einem",
|
|
"dieser",
|
|
"diese",
|
|
"dieses",
|
|
"nicht",
|
|
"mit",
|
|
"wenn",
|
|
"für",
|
|
"bei",
|
|
"kurz",
|
|
"kurzer",
|
|
"hinweis",
|
|
"zur",
|
|
"kontaktseite",
|
|
"webauftritt",
|
|
]);
|
|
|
|
const ENGLISH_MARKERS = new Set([
|
|
"the",
|
|
"and",
|
|
"you",
|
|
"your",
|
|
"we",
|
|
"our",
|
|
"is",
|
|
"are",
|
|
"was",
|
|
"were",
|
|
"to",
|
|
"of",
|
|
"in",
|
|
"for",
|
|
"on",
|
|
"with",
|
|
"this",
|
|
"that",
|
|
"it",
|
|
"from",
|
|
"have",
|
|
"has",
|
|
"will",
|
|
"can",
|
|
"if",
|
|
"quick",
|
|
"audit",
|
|
"bad",
|
|
"website",
|
|
"report",
|
|
]);
|
|
|
|
const OBSERVATION_TOKENS = [
|
|
/\b(mir|ich)\b[^\n]{0,80}\b(aufgefallen|festgestellt|bemerkt|beobachtet|gesehen|sichtbar)\b/i,
|
|
/\b(erkennt|zeigt|sichtbar|festgestellt|feststellen|feststellbar|finde|fällt)\b/i,
|
|
/\b(ich sehe|ich habe gesehen|bei der Prüfung)\b/i,
|
|
];
|
|
|
|
const SUGGESTION_TOKENS = [
|
|
/\b(empfehle|empfiehlt|vorschlage|vorschlagen|schlage vor|könnte helfen|kannst|können wir|sollte|sollten|ich könnte|ich würde|ich empfehle)\b/i,
|
|
/\b(schlage vor|schlage)\b/i,
|
|
/\b(?:mein(?:e[rmns]?)?\s+)?(?:konkreter\s+)?vorschlag(?:\s+ist)?\b/i,
|
|
/\b(ergänzt|ergänzen|anpassen|optimieren|verbessern|prüfen|einbauen|einzusetzen|setzten)\b/i,
|
|
];
|
|
|
|
const AI_SLOP_TOKENS = [
|
|
/\bmaßgeschneid(?:ert|ert|er)\b/i,
|
|
/\bnahtlos\b/i,
|
|
/\bstate[- ]of[- ]the[- ]art\b/i,
|
|
/\bgame[- ]?changer\b/i,
|
|
/\bsynerg(?:ie|istisch)\b/i,
|
|
/\brevolutionär\b/i,
|
|
/\bnext level\b/i,
|
|
/\bzukunftsweisend\b/i,
|
|
/\bdigital transformieren\b/i,
|
|
/\boutstanding\b/i,
|
|
/\bhebt.{0,20}Sichtbarkeit\b/i,
|
|
];
|
|
|
|
const HOSTILE_TOKENS = [
|
|
/\b(Ihr|Ihre|Sie|eure|euer)\b[^\n.!?]{0,80}\b(katastroph|schlecht|veraltet|unprofessionell|unbrauchbar|mangelhaft|chaotisch|desastr|desaster|skrupellos)\b/i,
|
|
/\b(ist|sind)\s+(?:total|absolut)\s+(?:schlecht|kaputt|katastroph)\b/i,
|
|
/\babsolut unprofessionell\b/i,
|
|
];
|
|
|
|
const SCORE_CONTEXT_TOKENS = [
|
|
/\b(?:pagespeed|lighthouse|score)\b[^\n]{0,120}\b\d{1,2}(?:[.,]\d+)?%?/i,
|
|
/\b\d{1,2}(?:[.,]\d+)?%?[^\n]{0,120}\b(?:pagespeed|lighthouse|score)\b/i,
|
|
];
|
|
|
|
const PRICE_PATTERNS = [
|
|
/\b\d{1,4}\s*(?:€|EUR|Euro|euro)/,
|
|
/(?:€|EUR|Euro|euro)\s*\d{1,4}(?:[.,]\d{1,2})?/,
|
|
/\b(?:preis|preise|kosten)\b[^a-z]{0,5}\d{1,4}\s*(?:€|EUR|Euro|euro)?/i,
|
|
];
|
|
|
|
const RAW_TECH_PATTERNS = [
|
|
/\braw\s*storage\s*id\b/i,
|
|
/\bstorage[_-]?id\b/i,
|
|
/\bmodel[_-]?id\b/i,
|
|
/\b(?:gpt|claude|gemini|llama|mistral|qwen|mixtral|deepseek|phi|sonar|gemma)\b[-\w]*/i,
|
|
/\{[^\n]{0,240}:[^\n]{0,240}\}/,
|
|
/\[[^\n]{0,240}\]/,
|
|
/\b[0-9a-f]{24}\b/i,
|
|
];
|
|
|
|
const EMAIL_TEMPLATE_PATTERNS = [
|
|
/\bich habe beobachtet\b/i,
|
|
/\bmir ist aufgefallen\b/i,
|
|
/\bich schlage vor\b/i,
|
|
/\bich empfehle\b/i,
|
|
];
|
|
|
|
const EMAIL_BROCHURE_PATTERNS = [
|
|
/\bmaßnahmen umsetzen\b/i,
|
|
/\bconversion[- ]rate steigern\b/i,
|
|
/\branking positiv beeinflussen\b/i,
|
|
/\babsprungraten senken\b/i,
|
|
/\bnachhaltig verbessern\b/i,
|
|
/\bsignifikant\b/i,
|
|
/\boptimierungspotenzial(?:e)?\b/i,
|
|
/\bnutzerzufriedenheit\b/i,
|
|
/\bsuchmaschinenplatzierung\b/i,
|
|
];
|
|
|
|
const EMAIL_AUDIT_TOPIC_PATTERNS = [
|
|
/\bmeta[- ]beschreibung\b/i,
|
|
/\bpage[- ]?speed\b/i,
|
|
/\bladezeit(?:en)?\b/i,
|
|
/\bkontaktformular\b/i,
|
|
/\bcall[- ]to[- ]action\b/i,
|
|
/\bmobile(?:n|r|s)? gerät/i,
|
|
/\bdesktop\b/i,
|
|
/\bh1[- ]?überschrift(?:en)?\b/i,
|
|
/\bbewertung(?:en)?\b/i,
|
|
/\bvertrauenssignal(?:e)?\b/i,
|
|
/\bstrukturierte daten\b/i,
|
|
];
|
|
|
|
const EMAIL_MINI_AUDIT_TRANSITIONS = [
|
|
/\baußerdem\b/i,
|
|
/\bzudem\b/i,
|
|
/\bein weiterer punkt\b/i,
|
|
/\bschließlich\b/i,
|
|
/\bdurch die umsetzung\b/i,
|
|
];
|
|
|
|
const EMAIL_LOW_FRICTION_ASK_PATTERNS = [
|
|
/\bsoll ich ihnen\b/i,
|
|
/\bwäre (?:das|ein kurzer hinweis)\b/i,
|
|
/\bdarf ich ihnen\b/i,
|
|
/\bkann ich ihnen\b/i,
|
|
/\boffen für\b/i,
|
|
];
|
|
|
|
const INFORMAL_EMAIL_ADDRESS_PATTERNS = [
|
|
/\bdu\b/i,
|
|
/\bdir\b/i,
|
|
/\bdein(?:e[rmns]?)?\b/i,
|
|
/\beuch\b/i,
|
|
/\beuer(?:e[rmns]?)?\b/i,
|
|
];
|
|
|
|
export type GermanCopyGuardIssue = {
|
|
field: string;
|
|
rule: string;
|
|
message: string;
|
|
};
|
|
|
|
export type GermanCopyGuardResult = {
|
|
passed: boolean;
|
|
issues: GermanCopyGuardIssue[];
|
|
};
|
|
|
|
export type AuditCopy = {
|
|
summary: string;
|
|
body: string;
|
|
};
|
|
|
|
export type EmailCopy = {
|
|
subject: string;
|
|
body: string;
|
|
};
|
|
|
|
export type CallScriptCopy = {
|
|
openingLine: string;
|
|
callScript: string[];
|
|
closeLine: string;
|
|
};
|
|
|
|
export type FollowUpCopy = {
|
|
message: string;
|
|
};
|
|
|
|
export type GermanCustomerCopy = {
|
|
auditSummary?: string;
|
|
auditBody?: string;
|
|
emailSubject?: string;
|
|
emailBody?: string;
|
|
callScript?: CallScriptCopy;
|
|
followUp?: string;
|
|
};
|
|
|
|
type ValidationOptions = {
|
|
requireIchForm?: boolean;
|
|
requireObservationAndSuggestion?: boolean;
|
|
skipIfTooShort?: boolean;
|
|
};
|
|
|
|
function addIssue(
|
|
issues: GermanCopyGuardIssue[],
|
|
field: string,
|
|
rule: string,
|
|
message: string,
|
|
) {
|
|
issues.push({ field, rule, message });
|
|
}
|
|
|
|
function tokenizeWords(value: string): string[] {
|
|
return value
|
|
.toLowerCase()
|
|
.match(/[a-zäöüß]{3,}/giu)
|
|
?.map((token) => token.toLowerCase()) ?? [];
|
|
}
|
|
|
|
function hasGermanAnchor(value: string): boolean {
|
|
const words = tokenizeWords(value);
|
|
|
|
if (!words.length) {
|
|
return true;
|
|
}
|
|
|
|
if (/[äöüß]/i.test(value)) {
|
|
return true;
|
|
}
|
|
|
|
const germanCount = words.reduce(
|
|
(count, word) => count + (GERMAN_MARKERS.has(word) ? 1 : 0),
|
|
0,
|
|
);
|
|
const englishCount = words.reduce(
|
|
(count, word) => count + (ENGLISH_MARKERS.has(word) ? 1 : 0),
|
|
0,
|
|
);
|
|
|
|
if (words.length <= 4) {
|
|
if (germanCount >= 1) {
|
|
return true;
|
|
}
|
|
return englishCount === 0;
|
|
}
|
|
|
|
if (germanCount >= 1) {
|
|
return true;
|
|
}
|
|
|
|
if (englishCount === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (englishCount / words.length >= 0.2) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function hasIchForm(value: string): boolean {
|
|
return /\b(ich|mich|mir|mein|meine|meinem|meiner)\b/i.test(value);
|
|
}
|
|
|
|
function hasObservation(value: string): boolean {
|
|
return OBSERVATION_TOKENS.some((pattern) => pattern.test(value));
|
|
}
|
|
|
|
function hasSuggestion(value: string): boolean {
|
|
return SUGGESTION_TOKENS.some((pattern) => pattern.test(value));
|
|
}
|
|
|
|
function hasAiSlop(value: string): boolean {
|
|
return AI_SLOP_TOKENS.some((pattern) => pattern.test(value));
|
|
}
|
|
|
|
function hasHostileTone(value: string): boolean {
|
|
return HOSTILE_TOKENS.some((pattern) => pattern.test(value));
|
|
}
|
|
|
|
function hasScoreArtifact(value: string): boolean {
|
|
return SCORE_CONTEXT_TOKENS.some((pattern) => pattern.test(value));
|
|
}
|
|
|
|
function hasPrice(value: string): boolean {
|
|
return PRICE_PATTERNS.some((pattern) => pattern.test(value));
|
|
}
|
|
|
|
function hasRawArtifact(value: string): boolean {
|
|
return RAW_TECH_PATTERNS.some((pattern) => pattern.test(value));
|
|
}
|
|
|
|
function countMatches(value: string, patterns: readonly RegExp[]) {
|
|
return patterns.reduce(
|
|
(count, pattern) => count + (pattern.test(value) ? 1 : 0),
|
|
0,
|
|
);
|
|
}
|
|
|
|
function countRegexMatches(value: string, pattern: RegExp) {
|
|
return value.match(pattern)?.length ?? 0;
|
|
}
|
|
|
|
function countSentences(value: string) {
|
|
return value
|
|
.split(/[.!?]+/)
|
|
.map((sentence) => sentence.trim())
|
|
.filter(Boolean).length;
|
|
}
|
|
|
|
function countParagraphs(value: string) {
|
|
return value
|
|
.trim()
|
|
.split(/\n\s*\n/)
|
|
.map((paragraph) => paragraph.trim())
|
|
.filter(Boolean).length;
|
|
}
|
|
|
|
function startsWithTemplateEmailPhrase(value: string) {
|
|
return new RegExp(
|
|
String.raw`^\s*(?:(?:guten tag|hallo|sehr geehrte[^,.!?]*|moin)[,.!?\s]+)?(?:${EMAIL_TEMPLATE_PATTERNS.map(
|
|
(pattern) => pattern.source,
|
|
).join("|")})`,
|
|
"i",
|
|
).test(value);
|
|
}
|
|
|
|
function hasLowFrictionAsk(value: string) {
|
|
return (
|
|
value.includes("?") &&
|
|
EMAIL_LOW_FRICTION_ASK_PATTERNS.some((pattern) => pattern.test(value))
|
|
);
|
|
}
|
|
|
|
function validateEmailSubjectTone(
|
|
issues: GermanCopyGuardIssue[],
|
|
subject: string,
|
|
) {
|
|
const trimmed = subject.trim();
|
|
if (!trimmed) {
|
|
return;
|
|
}
|
|
|
|
const words = tokenizeWords(trimmed);
|
|
const { subject: subjectRules } = customerToneGuidelines.email;
|
|
const hasInflatedSubject =
|
|
/optimierungspotenzial/i.test(trimmed) ||
|
|
/mehr sichtbarkeit/i.test(trimmed) ||
|
|
/bessere nutzererfahrung/i.test(trimmed) ||
|
|
/kundengewinnung/i.test(trimmed) ||
|
|
/conversion/i.test(trimmed) ||
|
|
/ranking/i.test(trimmed);
|
|
|
|
if (
|
|
trimmed.length > subjectRules.maxCharacters ||
|
|
words.length < subjectRules.minWords ||
|
|
words.length > subjectRules.maxWords ||
|
|
/:/.test(trimmed) ||
|
|
hasInflatedSubject
|
|
) {
|
|
addIssue(
|
|
issues,
|
|
"emailSubject",
|
|
"unnatural_email_subject",
|
|
"Betreff wirkt zu pitchig, zu lang oder nicht wie eine kurze Erstmail.",
|
|
);
|
|
}
|
|
}
|
|
|
|
function validateEmailBodyTone(
|
|
issues: GermanCopyGuardIssue[],
|
|
body: string,
|
|
) {
|
|
const trimmed = body.trim();
|
|
if (!trimmed) {
|
|
return;
|
|
}
|
|
|
|
const { email } = customerToneGuidelines;
|
|
const wordCount = tokenizeWords(trimmed).length;
|
|
if (wordCount < email.wordCount.min || wordCount > email.wordCount.max) {
|
|
addIssue(
|
|
issues,
|
|
"emailBody",
|
|
"unnatural_email_length",
|
|
"E-Mail sollte als Erstkontakt kurz bleiben: 60-130 Wörter.",
|
|
);
|
|
}
|
|
|
|
if (countSentences(trimmed) > email.maxSentences) {
|
|
addIssue(
|
|
issues,
|
|
"emailBody",
|
|
"too_many_email_sentences",
|
|
"E-Mail enthält zu viele Sätze für eine erste Kontaktaufnahme.",
|
|
);
|
|
}
|
|
|
|
if (countParagraphs(trimmed) > email.maxParagraphs) {
|
|
addIssue(
|
|
issues,
|
|
"emailBody",
|
|
"too_many_email_paragraphs",
|
|
"E-Mail sollte höchstens zwei kurze Absätze enthalten.",
|
|
);
|
|
}
|
|
|
|
const templatePhraseCount = EMAIL_TEMPLATE_PATTERNS.reduce(
|
|
(count, pattern) => count + countRegexMatches(trimmed, new RegExp(pattern.source, "gi")),
|
|
0,
|
|
);
|
|
const firstPersonCount = countRegexMatches(trimmed, /\bich\b/gi);
|
|
if (
|
|
startsWithTemplateEmailPhrase(trimmed) ||
|
|
templatePhraseCount >= 2 ||
|
|
firstPersonCount > 2
|
|
) {
|
|
addIssue(
|
|
issues,
|
|
"emailBody",
|
|
"formulaic_email_tone",
|
|
"E-Mail wirkt formelhaft; vermeide wiederholte Ich-habe-/Ich-schlage-vor-Muster.",
|
|
);
|
|
}
|
|
|
|
if (EMAIL_BROCHURE_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
addIssue(
|
|
issues,
|
|
"emailBody",
|
|
"brochure_email_language",
|
|
"E-Mail klingt nach Broschüre statt nach natürlicher Erstansprache.",
|
|
);
|
|
}
|
|
|
|
const topicCount = countMatches(trimmed, EMAIL_AUDIT_TOPIC_PATTERNS);
|
|
const transitionCount = countMatches(trimmed, EMAIL_MINI_AUDIT_TRANSITIONS);
|
|
if (topicCount >= 4 || transitionCount >= 2) {
|
|
addIssue(
|
|
issues,
|
|
"emailBody",
|
|
"email_reads_like_mini_audit",
|
|
"E-Mail bündelt zu viele Audit-Punkte und sollte höchstens zwei Befunde anreißen.",
|
|
);
|
|
}
|
|
|
|
if (INFORMAL_EMAIL_ADDRESS_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
addIssue(
|
|
issues,
|
|
"emailBody",
|
|
"informal_email_address",
|
|
"E-Mail sollte unbekannte lokale Betriebe formal mit Sie/Ihnen ansprechen.",
|
|
);
|
|
}
|
|
|
|
if (!hasLowFrictionAsk(trimmed)) {
|
|
addIssue(
|
|
issues,
|
|
"emailBody",
|
|
"missing_low_friction_ask",
|
|
"E-Mail sollte mit einer kurzen, leicht beantwortbaren Frage enden.",
|
|
);
|
|
}
|
|
}
|
|
|
|
function validateTextField(
|
|
issues: GermanCopyGuardIssue[],
|
|
field: string,
|
|
value: string,
|
|
options: ValidationOptions = {},
|
|
) {
|
|
if (options.skipIfTooShort && value.trim().length < 6) {
|
|
return;
|
|
}
|
|
|
|
if (!hasGermanAnchor(value)) {
|
|
addIssue(
|
|
issues,
|
|
field,
|
|
"not_german",
|
|
"Text wirkt nicht ausreichend deutsch.",
|
|
);
|
|
}
|
|
|
|
if (options.requireIchForm && !hasIchForm(value)) {
|
|
addIssue(
|
|
issues,
|
|
field,
|
|
"missing_ich_form",
|
|
"Text sollte in Ich-Form geschrieben sein.",
|
|
);
|
|
}
|
|
|
|
if (hasScoreArtifact(value)) {
|
|
addIssue(
|
|
issues,
|
|
field,
|
|
"pagespeed_score_artifact",
|
|
"Technische Score-/PageSpeed-Werte sollten nicht im Kunden-Text erscheinen.",
|
|
);
|
|
}
|
|
|
|
if (hasPrice(value)) {
|
|
addIssue(
|
|
issues,
|
|
field,
|
|
"price_mention",
|
|
"Preis- oder Währungsangaben sollten im Kunden-Text vermieden werden.",
|
|
);
|
|
}
|
|
|
|
if (hasAiSlop(value)) {
|
|
addIssue(
|
|
issues,
|
|
field,
|
|
"generic_ai_slop",
|
|
"Generische KI-Slop-Formulierungen erkannt.",
|
|
);
|
|
}
|
|
|
|
if (hasHostileTone(value)) {
|
|
addIssue(
|
|
issues,
|
|
field,
|
|
"hostile_tone",
|
|
"Anklagende oder negativ wertende Sprache wurde erkannt.",
|
|
);
|
|
}
|
|
|
|
if (hasRawArtifact(value)) {
|
|
addIssue(
|
|
issues,
|
|
field,
|
|
"raw_technical_artifact",
|
|
"Technische Artefakte im Text erkannt.",
|
|
);
|
|
}
|
|
|
|
if (options.requireObservationAndSuggestion && (!hasObservation(value) || !hasSuggestion(value))) {
|
|
addIssue(
|
|
issues,
|
|
field,
|
|
"missing_observation_or_suggestion",
|
|
"Beobachtung und Vorschlag sollten im gleichen Text erkennbar sein.",
|
|
);
|
|
}
|
|
}
|
|
|
|
function validateCallScriptText(
|
|
issues: GermanCopyGuardIssue[],
|
|
linePrefix: string,
|
|
scriptLine: string,
|
|
options: ValidationOptions,
|
|
) {
|
|
const lineValue = scriptLine?.trim();
|
|
if (!lineValue) {
|
|
return;
|
|
}
|
|
|
|
validateTextField(issues, linePrefix, lineValue, options);
|
|
}
|
|
|
|
export function validateAuditCopy(audit: AuditCopy): GermanCopyGuardResult {
|
|
const issues: GermanCopyGuardIssue[] = [];
|
|
|
|
validateTextField(issues, "auditSummary", audit.summary, {
|
|
requireIchForm: true,
|
|
requireObservationAndSuggestion: true,
|
|
});
|
|
validateTextField(issues, "auditBody", audit.body, {
|
|
requireIchForm: true,
|
|
requireObservationAndSuggestion: true,
|
|
});
|
|
|
|
return { passed: issues.length === 0, issues };
|
|
}
|
|
|
|
export function validateEmailCopy(email: EmailCopy): GermanCopyGuardResult {
|
|
const issues: GermanCopyGuardIssue[] = [];
|
|
|
|
validateTextField(issues, "emailSubject", email.subject, { skipIfTooShort: true });
|
|
validateEmailSubjectTone(issues, email.subject);
|
|
validateTextField(issues, "emailBody", email.body, {
|
|
requireIchForm: false,
|
|
requireObservationAndSuggestion: false,
|
|
});
|
|
validateEmailBodyTone(issues, email.body);
|
|
|
|
return { passed: issues.length === 0, issues };
|
|
}
|
|
|
|
export function validateCallScriptCopy(script: CallScriptCopy): GermanCopyGuardResult {
|
|
const issues: GermanCopyGuardIssue[] = [];
|
|
|
|
validateCallScriptText(issues, "callScript.openingLine", script.openingLine, {
|
|
requireIchForm: true,
|
|
});
|
|
validateCallScriptText(issues, "callScript.closeLine", script.closeLine, {
|
|
requireIchForm: false,
|
|
});
|
|
|
|
script.callScript.forEach((line, index) => {
|
|
validateCallScriptText(
|
|
issues,
|
|
`callScript.callScript[${index}]`,
|
|
line,
|
|
{
|
|
requireIchForm: false,
|
|
},
|
|
);
|
|
});
|
|
|
|
const scriptConcatenated = [
|
|
script.openingLine,
|
|
...script.callScript,
|
|
script.closeLine,
|
|
]
|
|
.filter((line) => line.trim().length > 0)
|
|
.join(" ");
|
|
|
|
if (!hasObservation(scriptConcatenated) || !hasSuggestion(scriptConcatenated)) {
|
|
addIssue(
|
|
issues,
|
|
"callScript",
|
|
"missing_observation_or_suggestion",
|
|
"Beobachtung und Vorschlag sollten im Call-Script erkennbar sein.",
|
|
);
|
|
}
|
|
|
|
return { passed: issues.length === 0, issues };
|
|
}
|
|
|
|
export function validateFollowUpCopy(followUp: FollowUpCopy): GermanCopyGuardResult {
|
|
const issues: GermanCopyGuardIssue[] = [];
|
|
|
|
validateTextField(issues, "followUp", followUp.message, {
|
|
requireIchForm: true,
|
|
requireObservationAndSuggestion: true,
|
|
});
|
|
|
|
return { passed: issues.length === 0, issues };
|
|
}
|
|
|
|
export function validateCustomerFacingCopy(input: GermanCustomerCopy): GermanCopyGuardResult {
|
|
const issues: GermanCopyGuardIssue[] = [];
|
|
|
|
if (input.auditSummary !== undefined) {
|
|
validateTextField(issues, "auditSummary", input.auditSummary, {
|
|
requireIchForm: true,
|
|
requireObservationAndSuggestion: true,
|
|
});
|
|
}
|
|
|
|
if (input.auditBody !== undefined) {
|
|
validateTextField(issues, "auditBody", input.auditBody, {
|
|
requireIchForm: true,
|
|
requireObservationAndSuggestion: true,
|
|
});
|
|
}
|
|
|
|
if (input.emailSubject !== undefined) {
|
|
validateTextField(issues, "emailSubject", input.emailSubject, {
|
|
skipIfTooShort: true,
|
|
});
|
|
validateEmailSubjectTone(issues, input.emailSubject);
|
|
}
|
|
|
|
if (input.emailBody !== undefined) {
|
|
validateTextField(issues, "emailBody", input.emailBody, {
|
|
requireIchForm: false,
|
|
requireObservationAndSuggestion: false,
|
|
});
|
|
validateEmailBodyTone(issues, input.emailBody);
|
|
}
|
|
|
|
if (input.callScript) {
|
|
issues.push(
|
|
...validateCallScriptCopy({
|
|
openingLine: input.callScript.openingLine,
|
|
callScript: [...input.callScript.callScript],
|
|
closeLine: input.callScript.closeLine,
|
|
}).issues,
|
|
);
|
|
}
|
|
|
|
if (input.followUp !== undefined) {
|
|
validateTextField(issues, "followUp", input.followUp, {
|
|
requireIchForm: true,
|
|
requireObservationAndSuggestion: true,
|
|
});
|
|
}
|
|
|
|
return { passed: issues.length === 0, issues };
|
|
}
|