*/ public static function schema() { $invitation = array( 'signer_name', 'link', 'sender_name', 'document_count', 'expiry_date' ); $otp = array( 'signer_name', 'otp_code', 'otp_ttl_minutes', 'sender_name' ); return array( 'signer_template' => array(), 'invitation_email_subject' => array( 'signer_name', 'sender_name', 'document_count' ), 'invitation_email_html' => $invitation, 'otp_email_subject' => array( 'sender_name' ), 'otp_email_html' => $otp, 'signer_title' => array( 'sender_name' ), 'signer_seal_btn' => array(), 'signer_refuse_btn' => array(), 'signer_otp_send' => array(), 'signer_otp_enter' => array(), 'signer_otp_resend' => array(), 'signer_success' => array( 'signer_name' ), 'signer_refused' => array( 'signer_name' ), ); } /** * Human label of a content key (admin editor). * * @param string $key Key. * @return string */ public static function key_label( $key ) { $labels = array( 'signer_template' => __( 'Signer page — presentation (HTML, use {equasign-viewer})', 'equasign' ), 'invitation_email_subject' => __( 'Invitation email — subject', 'equasign' ), 'invitation_email_html' => __( 'Invitation email — HTML body', 'equasign' ), 'otp_email_subject' => __( 'OTP email — subject', 'equasign' ), 'otp_email_html' => __( 'OTP email — HTML body', 'equasign' ), 'signer_title' => __( 'Signer page — title', 'equasign' ), 'signer_seal_btn' => __( 'Signer page — seal button', 'equasign' ), 'signer_refuse_btn' => __( 'Signer page — refuse button', 'equasign' ), 'signer_otp_send' => __( 'Signer page — send code button', 'equasign' ), 'signer_otp_enter' => __( 'Signer page — enter code label', 'equasign' ), 'signer_otp_resend' => __( 'Signer page — resend code link', 'equasign' ), 'signer_success' => __( 'Signer page — success message', 'equasign' ), 'signer_refused' => __( 'Signer page — refused message', 'equasign' ), ); return isset( $labels[ $key ] ) ? $labels[ $key ] : $key; } /* --------------------------------------------------------------------- */ /* Embedded defaults (native languages). */ /* --------------------------------------------------------------------- */ /** * Embedded default content for the mandatory `default` type, per native * language. English is the complete source set; each other native language * returns ONLY the keys it actually ships (see native_overrides()) — it does * NOT inherit the English set here, so that a missing key genuinely falls * through the resolution chain in get() (default-type override → * shop-default-language → embedded English) instead of short-circuiting to an * English value at the first candidate. * * @param string $lang Native language code. * @return array */ public static function embedded_default( $lang ) { if ( 'en' === $lang ) { return self::english_defaults(); } $over = self::native_overrides(); return isset( $over[ $lang ] ) && is_array( $over[ $lang ] ) ? $over[ $lang ] : array(); } /** * English source defaults (complete set). * * @return array */ public static function english_defaults() { return array( 'signer_template' => "

Please review the document(s) below and sign at the bottom of the page.

\n{equasign-viewer}", 'invitation_email_subject' => 'You have a document to sign', 'invitation_email_html' => "

Hello {{signer_name}},

\n

A document is waiting for your signature.

\n

Open and sign the document

", 'otp_email_subject' => 'Your signing code', 'otp_email_html' => "

Hello {{signer_name}},

\n

{{otp_code}}

\n

This code is valid for {{otp_ttl_minutes}} minutes. Enter it to seal your signature.

", 'signer_title' => 'Sign your document', 'signer_seal_btn' => 'Sign and seal', 'signer_refuse_btn' => 'I refuse to sign', 'signer_otp_send' => 'Send my code', 'signer_otp_enter' => 'Enter the code you received:', 'signer_otp_resend' => 'I did not receive anything', 'signer_success' => 'Thank you — your signature is sealed.', 'signer_refused' => 'You have declined to sign.', ); } /** * Per-native-language embedded default overrides (fr/es/de/pt). Every native * language ships a COMPLETE, natively-written default set for all content keys * (presentation template, notifications, signer labels) — a language panel is * shown in its own language, not English (English remains the source for extra, * operator-added languages and as the final resolution fallback). Overrides * stored by the operator still win, non-lossily. * * @return array> */ public static function native_overrides() { return array( 'fr' => array( 'signer_template' => "

Veuillez consulter le(s) document(s) ci-dessous et signer en bas de la page.

\n{equasign-viewer}", 'invitation_email_subject' => 'Vous avez un document à signer', 'invitation_email_html' => "

Bonjour {{signer_name}},

\n

Un document attend votre signature.

\n

Ouvrir et signer le document

", 'otp_email_subject' => 'Votre code de signature', 'otp_email_html' => "

Bonjour {{signer_name}},

\n

{{otp_code}}

\n

Ce code est valable {{otp_ttl_minutes}} minutes. Saisissez-le pour sceller votre signature.

", 'signer_title' => 'Signez votre document', 'signer_seal_btn' => 'Signer et sceller', 'signer_refuse_btn' => 'Je refuse de signer', 'signer_otp_send' => 'Envoyer mon code', // Non-breaking space before the colon (French typography) so the ":" // never wraps alone to the next line on the signer page label. 'signer_otp_enter' => "Saisissez le code que vous avez reçu\u{00A0}:", 'signer_otp_resend' => "Je n'ai rien reçu", 'signer_success' => 'Merci — votre signature est scellée.', 'signer_refused' => 'Vous avez refusé de signer.', ), 'es' => array( 'signer_template' => "

Revise el/los documento(s) a continuación y firme al final de la página.

\n{equasign-viewer}", 'invitation_email_subject' => 'Tiene un documento para firmar', 'invitation_email_html' => "

Hola {{signer_name}}:

\n

Un documento está esperando su firma.

\n

Abrir y firmar el documento

", 'otp_email_subject' => 'Su código de firma', 'otp_email_html' => "

Hola {{signer_name}}:

\n

{{otp_code}}

\n

Este código es válido durante {{otp_ttl_minutes}} minutos. Introdúzcalo para sellar su firma.

", 'signer_title' => 'Firme su documento', 'signer_seal_btn' => 'Firmar y sellar', 'signer_refuse_btn' => 'Me niego a firmar', 'signer_otp_send' => 'Enviar mi código', 'signer_otp_enter' => 'Introduzca el código que ha recibido:', 'signer_otp_resend' => 'No he recibido nada', 'signer_success' => 'Gracias — su firma ha quedado sellada.', 'signer_refused' => 'Ha rechazado firmar el documento.', ), 'de' => array( 'signer_template' => "

Bitte prüfen Sie das/die Dokument(e) unten und unterschreiben Sie am Ende der Seite.

\n{equasign-viewer}", 'invitation_email_subject' => 'Sie haben ein Dokument zu unterzeichnen', 'invitation_email_html' => "

Hallo {{signer_name}},

\n

Ein Dokument wartet auf Ihre Unterschrift.

\n

Dokument öffnen und unterzeichnen

", 'otp_email_subject' => 'Ihr Signaturcode', 'otp_email_html' => "

Hallo {{signer_name}},

\n

{{otp_code}}

\n

Dieser Code ist {{otp_ttl_minutes}} Minuten lang gültig. Geben Sie ihn ein, um Ihre Signatur zu besiegeln.

", 'signer_title' => 'Ihr Dokument unterzeichnen', 'signer_seal_btn' => 'Unterzeichnen und besiegeln', 'signer_refuse_btn' => 'Ich lehne die Unterzeichnung ab', 'signer_otp_send' => 'Meinen Code senden', 'signer_otp_enter' => 'Geben Sie den erhaltenen Code ein:', 'signer_otp_resend' => 'Ich habe nichts erhalten', 'signer_success' => 'Vielen Dank – Ihre Signatur ist besiegelt.', 'signer_refused' => 'Sie haben die Unterzeichnung abgelehnt.', ), 'pt' => array( 'signer_template' => "

Reveja o(s) documento(s) abaixo e assine no fim da página.

\n{equasign-viewer}", 'invitation_email_subject' => 'Tem um documento para assinar', 'invitation_email_html' => "

Olá {{signer_name}},

\n

Um documento aguarda a sua assinatura.

\n

Abra e assine o documento

", 'otp_email_subject' => 'O seu código de assinatura', 'otp_email_html' => "

Olá {{signer_name}},

\n

{{otp_code}}

\n

Este código é válido durante {{otp_ttl_minutes}} minutos. Introduza-o para selar a sua assinatura.

", 'signer_title' => 'Assine o seu documento', 'signer_seal_btn' => 'Assinar e selar', 'signer_refuse_btn' => 'Recuso-me a assinar', 'signer_otp_send' => 'Enviar o meu código', 'signer_otp_enter' => 'Introduza o código que recebeu:', 'signer_otp_resend' => 'Não recebi nada', 'signer_success' => 'Obrigado — a sua assinatura foi selada.', 'signer_refused' => 'Recusou-se a assinar.', ), ); } /* --------------------------------------------------------------------- */ /* Resolution. */ /* --------------------------------------------------------------------- */ /** * The stored overrides for a (type, lang), or an empty array. * * @param string $type Type slug. * @param string $lang Language code. * @return array */ public static function overrides( $type, $lang ) { $store = get_option( self::OPT, array() ); $type = EquaSign_Document_Types::sanitize_slug( $type ); $lang = EquaSign_Document_Types::normalize_lang( $lang ); if ( is_array( $store ) && isset( $store[ $type ][ $lang ] ) && is_array( $store[ $type ][ $lang ] ) ) { return $store[ $type ][ $lang ]; } return array(); } /** * Shop default language (falls back to the site locale, then 'en'). * * @return string */ public static function default_lang() { $lang = (string) apply_filters( 'equasign_default_language', get_option( 'equasign_default_language', '' ) ); if ( '' === $lang ) { $lang = (string) get_bloginfo( 'language' ); } $lang = EquaSign_Document_Types::normalize_lang( $lang ); return '' !== $lang ? $lang : 'en'; } /** * Raw content string for (type, lang, key), with the full fallback chain: * type+lang override → type+lang embedded → default-type+lang override → * default-type+lang embedded → default-type+shop-default-lang → default-type+en. * Always returns a string. * * @param string $type Type slug. * @param string $lang Language code. * @param string $key Content key. * @return string */ public static function get( $type, $lang, $key ) { $type = EquaSign_Document_Types::sanitize_slug( $type ); $lang = EquaSign_Document_Types::normalize_lang( $lang ); $def = EquaSign_Document_Types::DEFAULT_TYPE; $dl = self::default_lang(); $candidates = array( array( $type, $lang ), array( $def, $lang ), array( $def, $dl ), array( $def, 'en' ), ); foreach ( $candidates as $c ) { $ov = self::overrides( $c[0], $c[1] ); if ( isset( $ov[ $key ] ) && '' !== trim( (string) $ov[ $key ] ) ) { return (string) $ov[ $key ]; } $emb = self::embedded_default( $c[1] ); if ( isset( $emb[ $key ] ) && '' !== trim( (string) $emb[ $key ] ) ) { return (string) $emb[ $key ]; } } $en = self::english_defaults(); return isset( $en[ $key ] ) ? $en[ $key ] : ''; } /** * Renders a content key, substituting only whitelisted variables (HTML bodies * escape inserted values; `link` is URL-escaped). * * @param string $type Type slug. * @param string $lang Language code. * @param string $key Content key. * @param array $vars name => value. * @return string */ public static function render( $type, $lang, $key, array $vars = array() ) { $tmpl = self::get( $type, $lang, $key ); $schema = self::schema(); $allowed = isset( $schema[ $key ] ) ? $schema[ $key ] : array(); $html = in_array( $key, self::HTML_KEYS, true ); foreach ( $allowed as $name ) { if ( ! array_key_exists( $name, $vars ) ) { continue; } $value = (string) $vars[ $name ]; if ( $html ) { $value = ( 'link' === $name ) ? esc_url( $value ) : esc_html( $value ); } $tmpl = str_replace( '{{' . $name . '}}', $value, $tmpl ); } return $tmpl; } /* --------------------------------------------------------------------- */ /* Languages present for a type. */ /* --------------------------------------------------------------------- */ /** * The languages a type exposes in the editor: the native set plus any extra * language the operator has already created content for (on this type or the * default type). Preserves native order, appends extras. * * @param string $type Type slug. * @return string[] */ public static function languages_for( $type ) { $langs = self::LANGS; $store = get_option( self::OPT, array() ); $type = EquaSign_Document_Types::sanitize_slug( $type ); $seen = array(); if ( is_array( $store ) ) { foreach ( array( EquaSign_Document_Types::DEFAULT_TYPE, $type ) as $t ) { if ( isset( $store[ $t ] ) && is_array( $store[ $t ] ) ) { foreach ( array_keys( $store[ $t ] ) as $lc ) { $seen[ $lc ] = true; } } } } foreach ( array_keys( $seen ) as $lc ) { if ( ! in_array( $lc, $langs, true ) ) { $langs[] = $lc; } } return $langs; } /* --------------------------------------------------------------------- */ /* Persistence. */ /* --------------------------------------------------------------------- */ /** * Saves the content of a (type, lang). Only known keys are stored; an empty * value clears the override (falls back through the chain). Non-lossy: other * languages/types are untouched. HTML keys are kses'd, the rest plain text. * * @param string $type Type slug. * @param string $lang Language code. * @param array $values key => value. * @return void */ public static function save( $type, $lang, array $values ) { $type = EquaSign_Document_Types::sanitize_slug( $type ); $lang = EquaSign_Document_Types::normalize_lang( $lang ); if ( '' === $type || '' === $lang ) { return; } $store = get_option( self::OPT, array() ); $store = is_array( $store ) ? $store : array(); // Structural clamp (both builds, byte-identical): only a NATIVE language, or one // ALREADY ESTABLISHED for this type, may be edited here. A NEW non-native language must // be seeded FIRST — so a forged non-native model_lang with no prior entry is refused. // This is a data-integrity invariant that holds identically in both builds. $established = isset( $store[ $type ] ) && is_array( $store[ $type ] ) ? array_keys( $store[ $type ] ) : array(); if ( ! in_array( $lang, self::LANGS, true ) && ! in_array( $lang, $established, true ) ) { return; } if ( ! isset( $store[ $type ] ) || ! is_array( $store[ $type ] ) ) { $store[ $type ] = array(); } $clean = array(); $schema = self::schema(); foreach ( array_keys( $schema ) as $key ) { if ( ! isset( $values[ $key ] ) ) { continue; } $raw = (string) wp_unslash( $values[ $key ] ); if ( '' === trim( $raw ) ) { continue; // Empty → fall back through the chain (not stored). } $clean[ $key ] = in_array( $key, self::HTML_KEYS, true ) ? wp_kses_post( $raw ) : sanitize_textarea_field( $raw ); } if ( empty( $clean ) ) { unset( $store[ $type ][ $lang ] ); } else { $store[ $type ][ $lang ] = $clean; } update_option( self::OPT, $store, false ); } }