'Tiếng Việt', 'en' => 'English', 'zh' => '中文(简体)', 'ja' => '日本語', 'ko' => '한국어', 'th' => 'ไทย', 'id' => 'Bahasa Indonesia', 'fr' => 'Français', 'de' => 'Deutsch', 'es' => 'Español', 'pt' => 'Português', 'ru' => 'Русский', ); /** * Short label shown on the selector button. * * @var array */ const SHORT = array( 'vi' => 'VI', 'en' => 'EN', 'zh' => '中文', 'ja' => '日本語', 'ko' => '한국어', 'th' => 'ไทย', 'id' => 'ID', 'fr' => 'FR', 'de' => 'DE', 'es' => 'ES', 'pt' => 'PT', 'ru' => 'RU', ); /** * The tag the browser's speech recognition is asked for, per language (1.13.0). * * The widget's language codes are two letters (`vi`, `zh`), while the Web Speech API wants a * BCP-47 tag (`vi-VN`, `zh-CN`) — and it does not guess: given `zh` it listens for nothing useful * and returns nothing, with no error. So the mapping lives HERE, with the rest of the language * data, rather than as twelve literals inside the widget script. * * `pt-PT` rather than `pt-BR` because the Portuguese translations in this plugin follow European * Portuguese; a site that would rather have Brazil can correct one tag with the * `mybot_speech_tags` filter instead of editing core. * * @var array */ const SPEECH_TAGS = array( 'vi' => 'vi-VN', 'en' => 'en-US', 'zh' => 'zh-CN', 'ja' => 'ja-JP', 'ko' => 'ko-KR', 'th' => 'th-TH', 'id' => 'id-ID', 'fr' => 'fr-FR', 'de' => 'de-DE', 'es' => 'es-ES', 'pt' => 'pt-PT', 'ru' => 'ru-RU', ); /** * Every language's speech tag, for the widget. * * @return array Language code to BCP-47 tag. */ public static function speech_tags() { /** * Filters the speech-recognition tag for each language. * * @param array $tags Language code to BCP-47 tag. */ return (array) apply_filters( 'mybot_speech_tags', self::SPEECH_TAGS ); } /** * Courtesy phrases — greetings, thanks, acknowledgements and farewells, per language (1.18.0). * * WHY THIS TABLE EXISTS * * The knowledge lock refuses anything it cannot answer from the website, which is correct — and it * answered **"thanks for your support"** with *"this question is outside the information this website * holds"*. The visitor had not asked a question. The length rule (`< 15 characters`) caught "Hi" and missed * every polite sentence longer than that, so the more polite the visitor, the worse the reply. * * So politeness gets its own list. Three rules make it safe to keep INSIDE the lock rather than beside it: * * 1. **A phrase must match from the START** of the message, and what follows may only be filler * ("for your support", "bạn nhiều", "so much"). Anything else means the visitor is asking something, * and it goes back to the normal path. * 2. **No digits, no question mark, no question word** in the message at all — `is_courtesy()` refuses * those, so "Hi, giá bảo hành bao nhiêu?" is a question that happens to start politely, not a greeting. * 3. **The answer contains no facts.** The lock's small-talk note already forbids them; a courtesy reply * is a warm sentence and an offer to help, nothing more. * * Every language carries its own list because a visitor writes in their language, not in the site's. * * @var array */ const COURTESY = array( 'vi' => array( 'xin chào', 'chào bạn', 'chào', 'hi', 'hello', 'cảm ơn', 'cám ơn', 'cảm ơn bạn', 'cảm ơn bạn đã hỗ trợ', 'cảm ơn nhé', 'ok', 'oke', 'okay', 'được', 'vâng', 'dạ', 'ừ', 'tạm biệt', 'chào nhé' ), 'en' => array( 'hi', 'hello', 'hey', 'good morning', 'good afternoon', 'good evening', 'thanks', 'thank you', 'thanks for your support', 'thank you for your help', 'thanks a lot', 'many thanks', 'ok', 'okay', 'got it', 'sure', 'bye', 'goodbye', 'see you' ), 'zh' => array( '你好', '您好', '嗨', '谢谢', '谢谢你', '谢谢你的帮助', '多谢', '好的', '好', '可以', '再见', '拜拜' ), 'ja' => array( 'こんにちは', 'おはようございます', 'こんばんは', 'ありがとう', 'ありがとうございます', 'どうも', 'はい', 'わかりました', 'オーケー', 'さようなら', 'またね' ), 'ko' => array( '안녕하세요', '안녕', '감사합니다', '고마워요', '고맙습니다', '네', '알겠습니다', '오케이', '안녕히 계세요', '잘 가요' ), 'th' => array( 'สวัสดี', 'สวัสดีครับ', 'สวัสดีค่ะ', 'ขอบคุณ', 'ขอบคุณครับ', 'ขอบคุณค่ะ', 'ขอบใจ', 'โอเค', 'ได้', 'ลาก่อน', 'บาย' ), 'id' => array( 'halo', 'hai', 'selamat pagi', 'selamat siang', 'terima kasih', 'terima kasih banyak', 'makasih', 'oke', 'ok', 'baik', 'siap', 'sampai jumpa', 'dadah' ), 'fr' => array( 'bonjour', 'salut', 'bonsoir', 'merci', 'merci beaucoup', 'merci pour votre aide', 'd’accord', 'daccord', 'ok', 'oui', 'au revoir', 'à bientôt' ), 'de' => array( 'hallo', 'hi', 'guten tag', 'guten morgen', 'guten abend', 'danke', 'danke schön', 'vielen dank', 'danke für ihre hilfe', 'okay', 'ok', 'ja', 'auf wiedersehen', 'bis bald' ), 'es' => array( 'hola', 'buenos días', 'buenas tardes', 'buenas', 'gracias', 'muchas gracias', 'gracias por su ayuda', 'vale', 'ok', 'de acuerdo', 'adiós', 'hasta luego' ), 'pt' => array( 'olá', 'oi', 'bom dia', 'boa tarde', 'obrigado', 'obrigada', 'muito obrigado', 'obrigado pela ajuda', 'ok', 'está bem', 'tudo bem', 'adeus', 'até logo' ), 'ru' => array( 'привет', 'здравствуйте', 'добрый день', 'доброе утро', 'спасибо', 'большое спасибо', 'спасибо за помощь', 'хорошо', 'ок', 'да', 'до свидания', 'пока' ), ); /** * Words a courtesy message may continue with, and still be courtesy. * * "thanks for your support" is "thanks" plus filler; "thanks, what is the price" is "thanks" plus a * question, and the question words below are what separate them. Kept deliberately small: a word that is * not here and not in the phrase itself sends the message back to the normal path, which is the safe * direction to fail. * * @var array */ const COURTESY_FILLERS = array( 'for', 'your', 'you', 'so', 'much', 'very', 'a', 'lot', 'again', 'too', 'all', 'the', 'help', 'support', 'advice', 'time', 'bạn', 'nhiều', 'nhé', 'ạ', 'rất', 'đã', 'giúp', 'hỗ', 'trợ', 'và', 'cảm', 'ơn', 'cho', 'votre', 'aide', 'beaucoup', 'pour', 'ihre', 'hilfe', 'sehr', 'por', 'su', 'ayuda', 'pela', 'за', 'помощь' ); /** * Words that mean "I am asking something", in the languages this plugin speaks. * * The lock must never answer a question with a warm sentence and no facts, so a message containing any of * these is NOT courtesy, however politely it begins. * * @var array */ const QUESTION_WORDS = array( 'how', 'what', 'when', 'where', 'why', 'which', 'who', 'can', 'could', 'do', 'does', 'did', 'is', 'are', 'price', 'cost', 'warranty', 'gia', 'giá', 'bao', 'nhiêu', 'sao', 'nào', 'đâu', 'khi', 'thế', 'làm', 'có', 'không', 'được', 'how much', 'combien', 'pourquoi', 'quand', 'wann', 'wie', 'warum', 'cuanto', 'cuánto', 'como', 'cómo', 'quanto', 'сколько', 'как', 'когда', 'почему', '多少钱', '怎么', '为什么', '什么', 'いくら', 'どう', 'なぜ', '何', '얼마', '어떻게', '왜', '무엇', 'เท่าไหร่', 'อย่างไร', 'ทำไม', 'อะไร', 'berapa', 'bagaimana', 'kenapa', 'apa' ); /** * Every courtesy phrase from every language, flattened. * * @return array */ public static function courtesy_phrases() { $phrases = array(); foreach ( self::COURTESY as $list ) { foreach ( (array) $list as $phrase ) { $phrases[] = $phrase; } } /** * Filters the phrases the lock still answers as politeness rather than refusing. * * A site whose visitors are polite in a way this list does not know can add the phrase here instead of * waiting for a release — the same door `mybot_model_capability` opens for a model list. * * @param array $phrases Lower-case phrases, any language. */ return (array) apply_filters( 'mybot_courtesy_phrases', $phrases ); } /** * The speech tag for one language, falling back to English. * * A browser with no recognition support never asks, so a missing tag costs nothing here — but it * must not be an empty string, because `recognition.lang = ''` makes the API listen in the * browser's own locale while the visitor is looking at a Vietnamese screen. * * @param string $code Language code. * @return string */ public static function speech_tag( $code ) { $tags = self::speech_tags(); $code = sanitize_key( $code ); return isset( $tags[ $code ] ) ? (string) $tags[ $code ] : 'en-US'; } /** * Instruction that makes the LLM answer in the right language. * * Until 1.7.0 every line here said the opposite of what the site owner asked for: *"Always answer * in English, even if the customer writes in another language."* The widget's language switcher * therefore decided the whole conversation, and a visitor who typed in Vietnamese into a widget * sitting on EN got a correct, well-cited answer — in the wrong language. Nobody sees an error; * they see a chatbot that did not notice them. * * The rule now: the visitor's OWN WORDS decide. That is deliberately a sentence to the model * rather than a language guess in code, because the model identifies language far better than a * regular expression can — it also handles a message with no diacritics ("toi can tim hieu bao * hanh"), which no accent test can read. Each line still names its own language, which is what * the answer falls back to when a message carries no signal at all ("ok", "?", "iphone"). * * @var array */ const INSTRUCTIONS = array( 'vi' => 'Trả lời bằng đúng ngôn ngữ mà khách đã dùng trong tin nhắn gần nhất, kể cả khi ngôn ngữ đó khác với ngôn ngữ của hướng dẫn này. Nếu tin nhắn quá ngắn để nhận ra ngôn ngữ, hãy trả lời bằng tiếng Việt.', 'en' => 'Answer in the language the customer used in their latest message, even when that differs from the language of this instruction. If the message is too short to tell which language it is, answer in English.', 'zh' => '请用客户最近一条消息所用的语言回答,即使该语言与本说明的语言不同。如果消息太短、无法判断语言,请用简体中文回答。', 'ja' => 'お客様の最新のメッセージと同じ言語で回答してください。この指示の言語と異なっていてもかまいません。メッセージが短すぎて言語を判別できない場合は、日本語で回答してください。', 'ko' => '고객의 최신 메시지와 같은 언어로 답변하세요. 이 지침의 언어와 달라도 됩니다. 메시지가 너무 짧아 언어를 판단할 수 없으면 한국어로 답변하세요.', 'th' => 'ตอบด้วยภาษเดียวกับที่ลูกค้าใช้ในข้อความล่าสุด แม้จะต่างจากภาษาของคำสั่งนี้ หากข้อความสั้นเกินไปจนบอกภาษาไม่ได้ ให้ตอบเป็นภาษาไทย', 'id' => 'Jawab dalam bahasa yang dipakai pelanggan pada pesan terakhirnya, meskipun berbeda dari bahasa instruksi ini. Jika pesannya terlalu pendek untuk dikenali bahasanya, jawab dalam Bahasa Indonesia.', 'fr' => 'Répondez dans la langue utilisée par le client dans son dernier message, même si elle diffère de la langue de cette consigne. Si le message est trop court pour déterminer la langue, répondez en français.', 'de' => 'Antworten Sie in der Sprache, die der Kunde in seiner letzten Nachricht verwendet hat, auch wenn sie von der Sprache dieser Anweisung abweicht. Ist die Nachricht zu kurz, um die Sprache zu erkennen, antworten Sie auf Deutsch.', 'es' => 'Responde en el idioma que el cliente usó en su último mensaje, aunque sea distinto del idioma de esta instrucción. Si el mensaje es demasiado corto para reconocer el idioma, responde en español.', 'pt' => 'Responda no idioma que o cliente usou na última mensagem, mesmo que seja diferente do idioma desta instrução. Se a mensagem for curta demais para identificar o idioma, responda em português.', 'ru' => 'Отвечайте на том языке, который клиент использовал в последнем сообщении, даже если он отличается от языка этой инструкции. Если сообщение слишком короткое, чтобы определить язык, отвечайте на русском.', ); /** * Per-language RAG note. * * @var array */ const RAG_NOTES = array( 'vi' => 'Dưới đây là thông tin từ website. Chỉ trả lời dựa trên thông tin này. Nếu thông tin không đủ, hãy nói rõ là chưa có thông tin và đề nghị khách liên hệ trực tiếp.', 'en' => 'Below is information from the website. Answer only based on this information. If it is not enough, say clearly that the information is unavailable and ask the customer to contact you directly.', 'zh' => '以下是网站上的信息。请仅根据这些信息回答。如果信息不足,请明确说明暂无相关信息,并请客户直接联系。', 'ja' => '以下はサイト上の情報です。この情報のみに基づいて回答してください。情報が不足している場合は、情報がない旨を伝え、直接のお問い合わせを案内してください。', 'ko' => '아래는 웹사이트의 정보입니다. 이 정보에만 근거하여 답변하세요. 정보가 부족하면 정보가 없음을 알리고 직접 문의하도록 안내하세요.', 'th' => 'นี่คือข้อมูลจากเว็บไซต์ ให้ตอบโดยอิงจากข้อมูลนี้เท่านั้น หากข้อมูลไม่พอ ให้แจ้งว่าไม่มีข้อมูลและแนะนำให้ติดต่อโดยตรง', 'id' => 'Berikut informasi dari situs web. Jawab hanya berdasarkan informasi ini. Jika kurang, katakan dengan jelas bahwa informasinya belum tersedia dan minta pelanggan menghubungi langsung.', 'fr' => "Voici des informations du site. Répondez uniquement à partir de ces informations. Si elles sont insuffisantes, indiquez clairement que l'information n'est pas disponible et invitez le client à vous contacter directement.", 'de' => 'Hier sind Informationen von der Website. Antworten Sie nur auf Basis dieser Informationen. Wenn sie nicht ausreichen, sagen Sie klar, dass die Information nicht vorliegt, und bitten Sie den Kunden, direkt Kontakt aufzunehmen.', 'es' => 'A continuación, información del sitio web. Responde solo con esta información. Si no es suficiente, indica claramente que no hay información y pide al cliente que contacte directamente.', 'pt' => 'Abaixo estão informações do site. Responda apenas com base nessas informações. Se forem insuficientes, diga claramente que a informação não está disponível e peça ao cliente para entrar em contato diretamente.', 'ru' => 'Ниже приведена информация с сайта. Отвечайте только на её основе. Если информации недостаточно, прямо сообщите об этом и предложите клиенту связаться напрямую.', ); /** * Widget interface strings that are NOT part of the owner's content, per language. * * Until 1.9.0 these sentences came from `__()` calls in the PHP that renders the widget, so they * followed the LOCALE OF THE SITE while everything around them — the greeting, the placeholder, * the answer — followed the language the visitor picked. Pick Japanese on an English site and the * content was Japanese while the buttons said "Send", "Finding an answer…", "Sources", "Helpful" * and "Open chat" (docs/08 muc 6.38). The same two-halves-disagree shape as 6.37, one layer * further out. * * 1.9.0 fixed eleven of those sentences and left THREE behind, all of them invisible to a sighted * reader: the × button's label, the input's screen-reader label and the floating launcher's label * (docs/08 muc 6.39). The fix was right and narrower than its own name, and the guard rule could * not see it because it only read the JavaScript — `inputLabel` is one of the three, which is why * the table is twelve keys and not eleven. * * They live here rather than in the .po catalogue for the same reason the hand-over sentences do: * a .po file is loaded for the ADMIN language, and these are read by VISITORS in the language the * visitor chose. `ui_strings()` merges over English, so a missing line falls back rather than * disappearing, and a check in tools/check-language-follow.mjs refuses to let a language ship with * a gap. * * @var array */ const UI_STRINGS = array( 'en' => array( 'send' => 'Send', 'voiceStart' => 'Speak your question', 'voiceStop' => 'Stop listening', 'attach' => 'Send a picture', 'attachRemove' => 'Remove the picture', 'attachFailed' => 'That picture could not be prepared. Please use a JPEG, PNG or WebP picture.', 'attachNeeds' => 'Please type your question as well — the picture is sent together with it.', 'open' => 'Open chat', 'close' => 'Close chat', 'thinking' => 'Finding an answer...', 'searching' => 'Searching the knowledge base...', 'error' => 'Something went wrong. Please try again.', 'sources' => 'Sources', 'helpful' => 'Helpful', 'notHelpful' => 'Not helpful', 'thanks' => 'Thanks for your feedback!', 'inputLabel' => 'Enter your question', 'language' => 'Language', 'lockedRefusal' => 'This question is outside the information this website holds, so I do not have anything to answer it with. Please leave your email address and a member of staff will reply to you directly.', 'contact' => 'Leave your details', 'contactTitle' => 'Leave your details so we can follow up', 'contactIntro' => 'This is optional. It goes only to the staff of this website.', 'contactPhone' => 'Phone', 'contactEmail' => 'Email', 'contactConsent' => 'I agree that this website may contact me about my question.', 'contactSend' => 'Send', 'contactDone' => 'Thank you. We will get back to you.', 'contactMissing' => 'Please leave a phone number or an email address.', 'contactError' => 'Your details could not be sent. Please try again in a few minutes.', ), 'vi' => array( 'send' => 'Gửi', 'voiceStart' => 'Nói câu hỏi của bạn', 'voiceStop' => 'Dừng nghe', 'attach' => 'Gửi ảnh', 'attachRemove' => 'Bỏ ảnh', 'attachFailed' => 'Không xử lý được ảnh này. Vui lòng dùng ảnh JPEG, PNG hoặc WebP.', 'attachNeeds' => 'Bạn nhập thêm câu hỏi giúp tôi nhé — ảnh sẽ được gửi kèm theo câu hỏi.', 'open' => 'Mở khung chat', 'close' => 'Đóng khung chat', 'thinking' => 'Đang tìm câu trả lời...', 'searching' => 'Đang tra cứu kho tri thức...', 'error' => 'Có lỗi xảy ra. Vui lòng thử lại.', 'sources' => 'Nguồn', 'helpful' => 'Hữu ích', 'notHelpful' => 'Không hữu ích', 'thanks' => 'Cảm ơn bạn đã đánh giá!', 'inputLabel' => 'Nhập câu hỏi của bạn', 'language' => 'Ngôn ngữ', 'lockedRefusal' => 'Câu hỏi này nằm ngoài phần thông tin mà website có, nên tôi chưa có dữ liệu để trả lời. Bạn để lại email giúp tôi nhé, nhân viên sẽ trả lời trực tiếp.', 'contact' => 'Để lại thông tin', 'contactTitle' => 'Để lại thông tin để được hỗ trợ tiếp', 'contactIntro' => 'Phần này không bắt buộc. Thông tin chỉ tới nhân viên của website này.', 'contactPhone' => 'Số điện thoại', 'contactEmail' => 'Email', 'contactConsent' => 'Tôi đồng ý để website này liên hệ với tôi về câu hỏi của tôi.', 'contactSend' => 'Gửi thông tin', 'contactDone' => 'Cảm ơn bạn. Chúng tôi sẽ liên hệ lại.', 'contactMissing' => 'Bạn để lại số điện thoại hoặc email giúp tôi nhé.', 'contactError' => 'Chưa gửi được thông tin. Bạn thử lại sau vài phút nhé.', ), 'zh' => array( 'send' => '发送', 'voiceStart' => '说出您的问题', 'voiceStop' => '停止聆听', 'attach' => '发送图片', 'attachRemove' => '移除图片', 'attachFailed' => '无法处理这张图片。请使用 JPEG、PNG 或 WebP 格式的图片。', 'attachNeeds' => '请同时输入您的问题,图片会一起发送。', 'open' => '打开聊天', 'close' => '关闭聊天', 'thinking' => '正在寻找答案...', 'searching' => '正在检索知识库...', 'error' => '出错了,请重试。', 'sources' => '来源', 'helpful' => '有帮助', 'notHelpful' => '没有帮助', 'thanks' => '感谢您的反馈!', 'inputLabel' => '请输入您的问题', 'language' => '语言', 'lockedRefusal' => '这个问题超出了本站已有信息的范围,我这里没有可以回答的资料。请留下您的邮箱,我们的同事会直接回复您。', 'contact' => '留下联系方式', 'contactTitle' => '留下联系方式,方便我们后续联系', 'contactIntro' => '此项可不填。信息只会发送给本站工作人员。', 'contactPhone' => '电话', 'contactEmail' => '邮箱', 'contactConsent' => '我同意本站就我的问题与我联系。', 'contactSend' => '发送', 'contactDone' => '谢谢,我们会尽快联系您。', 'contactMissing' => '请留下电话号码或邮箱。', 'contactError' => '信息未能发送,请几分钟后再试。', ), 'ja' => array( 'send' => '送信', 'voiceStart' => '質問を話してください', 'voiceStop' => '聞き取りを停止', 'attach' => '写真を送る', 'attachRemove' => '写真を削除', 'attachFailed' => 'この写真を準備できませんでした。JPEG、PNG、WebP の写真をお使いください。', 'attachNeeds' => '質問も入力してください。写真は一緒に送信されます。', 'open' => 'チャットを開く', 'close' => 'チャットを閉じる', 'thinking' => '回答を探しています...', 'searching' => 'ナレッジベースを検索しています...', 'error' => 'エラーが発生しました。もう一度お試しください。', 'sources' => '出典', 'helpful' => '役に立った', 'notHelpful' => '役に立たなかった', 'thanks' => 'フィードバックありがとうございます!', 'inputLabel' => 'ご質問を入力してください', 'language' => '言語', 'lockedRefusal' => 'この質問は当サイトが持つ情報の範囲外のため、お答えできる資料がありません。メールアドレスをご記入いただければ、担当者より直接ご連絡いたします。', 'contact' => '連絡先を残す', 'contactTitle' => 'あとでご連絡できるよう、連絡先をお知らせください', 'contactIntro' => '任意です。このサイトの担当者だけが閲覧します。', 'contactPhone' => '電話番号', 'contactEmail' => 'メールアドレス', 'contactConsent' => 'このサイトから問い合わせについて連絡を受けることに同意します。', 'contactSend' => '送信', 'contactDone' => 'ありがとうございます。折り返しご連絡します。', 'contactMissing' => '電話番号かメールアドレスをご記入ください。', 'contactError' => '送信できませんでした。数分後にもう一度お試しください。', ), 'ko' => array( 'send' => '보내기', 'voiceStart' => '질문을 말하세요', 'voiceStop' => '듣기 중지', 'attach' => '사진 보내기', 'attachRemove' => '사진 삭제', 'attachFailed' => '이 사진을 준비할 수 없습니다. JPEG, PNG, WebP 사진을 사용해 주세요.', 'attachNeeds' => '질문도 함께 입력해 주세요. 사진은 함께 전송됩니다.', 'open' => '채팅 열기', 'close' => '채팅 닫기', 'thinking' => '답변을 찾는 중...', 'searching' => '지식 베이스를 검색하는 중...', 'error' => '문제가 발생했습니다. 다시 시도해 주세요.', 'sources' => '출처', 'helpful' => '도움이 됨', 'notHelpful' => '도움이 되지 않음', 'thanks' => '의견을 보내 주셔서 감사합니다!', 'inputLabel' => '질문을 입력하세요', 'language' => '언어', 'lockedRefusal' => '이 질문은 이 웹사이트가 보유한 정보의 범위를 벗어나므로 답변할 자료가 없습니다. 이메일을 남겨 주시면 담당자가 직접 답변드리겠습니다.', 'contact' => '연락처 남기기', 'contactTitle' => '나중에 연락드릴 수 있도록 연락처를 남겨 주세요', 'contactIntro' => '선택 사항입니다. 이 사이트의 담당자만 볼 수 있습니다.', 'contactPhone' => '전화번호', 'contactEmail' => '이메일', 'contactConsent' => '문의와 관련해 이 사이트가 저에게 연락하는 것에 동의합니다.', 'contactSend' => '보내기', 'contactDone' => '감사합니다. 곧 연락드리겠습니다.', 'contactMissing' => '전화번호나 이메일을 남겨 주세요.', 'contactError' => '보내지 못했습니다. 몇 분 후 다시 시도해 주세요.', ), 'th' => array( 'send' => 'ส่ง', 'voiceStart' => 'พูดคำถามของคุณ', 'voiceStop' => 'หยุดฟัง', 'attach' => 'ส่งรูปภาพ', 'attachRemove' => 'ลบรูปภาพ', 'attachFailed' => 'ไม่สามารถเตรียมรูปภาพนี้ได้ กรุณาใช้รูป JPEG, PNG หรือ WebP', 'attachNeeds' => 'กรุณาพิมพ์คำถามด้วย รูปภาพจะถูกส่งไปพร้อมกัน', 'open' => 'เปิดแชท', 'close' => 'ปิดแชท', 'thinking' => 'กำลังค้นหาคำตอบ...', 'searching' => 'กำลังค้นฐานความรู้...', 'error' => 'เกิดข้อผิดพลาด กรุณาลองใหม่อีกครั้ง', 'sources' => 'แหล่งข้อมูล', 'helpful' => 'มีประโยชน์', 'notHelpful' => 'ไม่มีประโยชน์', 'thanks' => 'ขอบคุณสำหรับความคิดเห็น!', 'inputLabel' => 'พิมพ์คำถามของคุณ', 'language' => 'ภาษา', 'lockedRefusal' => 'คำถามนี้อยู่นอกเหนือข้อมูลที่เว็บไซต์นี้มี จึงไม่มีข้อมูลสำหรับตอบ กรุณาฝากอีเมลไว้ เจ้าหน้าที่จะตอบคุณโดยตรง', 'contact' => 'ฝากข้อมูลติดต่อ', 'contactTitle' => 'ฝากข้อมูลติดต่อไว้เพื่อให้เราติดตามผล', 'contactIntro' => 'ไม่บังคับ ข้อมูลจะถึงเฉพาะเจ้าหน้าที่ของเว็บไซต์นี้', 'contactPhone' => 'โทรศัพท์', 'contactEmail' => 'อีเมล', 'contactConsent' => 'ฉันยินยอมให้เว็บไซต์นี้ติดต่อกลับเกี่ยวกับคำถามของฉัน', 'contactSend' => 'ส่งข้อมูล', 'contactDone' => 'ขอบคุณ เราจะติดต่อกลับ', 'contactMissing' => 'กรุณาฝากเบอร์โทรหรืออีเมล', 'contactError' => 'ส่งข้อมูลไม่สำเร็จ กรุณาลองใหม่ในอีกสักครู่', ), 'id' => array( 'send' => 'Kirim', 'voiceStart' => 'Ucapkan pertanyaan Anda', 'voiceStop' => 'Berhenti mendengarkan', 'attach' => 'Kirim gambar', 'attachRemove' => 'Hapus gambar', 'attachFailed' => 'Gambar ini tidak dapat disiapkan. Gunakan gambar JPEG, PNG, atau WebP.', 'attachNeeds' => 'Tuliskan juga pertanyaan Anda — gambar dikirim bersama pertanyaan itu.', 'open' => 'Buka obrolan', 'close' => 'Tutup obrolan', 'thinking' => 'Mencari jawaban...', 'searching' => 'Menelusuri basis pengetahuan...', 'error' => 'Terjadi kesalahan. Silakan coba lagi.', 'sources' => 'Sumber', 'helpful' => 'Membantu', 'notHelpful' => 'Tidak membantu', 'thanks' => 'Terima kasih atas masukannya!', 'inputLabel' => 'Masukkan pertanyaan Anda', 'language' => 'Bahasa', 'lockedRefusal' => 'Pertanyaan ini di luar informasi yang dimiliki situs ini, jadi saya tidak punya bahan untuk menjawabnya. Silakan tinggalkan email, staf kami akan membalas langsung.', 'contact' => 'Tinggalkan kontak', 'contactTitle' => 'Tinggalkan kontak agar kami bisa menindaklanjuti', 'contactIntro' => 'Ini opsional. Hanya staf situs ini yang melihatnya.', 'contactPhone' => 'Telepon', 'contactEmail' => 'Email', 'contactConsent' => 'Saya setuju situs ini menghubungi saya tentang pertanyaan saya.', 'contactSend' => 'Kirim', 'contactDone' => 'Terima kasih. Kami akan menghubungi Anda.', 'contactMissing' => 'Mohon tinggalkan nomor telepon atau email.', 'contactError' => 'Kontak tidak dapat dikirim. Coba lagi beberapa menit lagi.', ), 'fr' => array( 'send' => 'Envoyer', 'voiceStart' => 'Dictez votre question', 'voiceStop' => 'Arrêter l’écoute', 'attach' => 'Envoyer une photo', 'attachRemove' => 'Retirer la photo', 'attachFailed' => 'Cette photo n’a pas pu être préparée. Utilisez une photo JPEG, PNG ou WebP.', 'attachNeeds' => 'Veuillez aussi écrire votre question — la photo est envoyée avec elle.', 'open' => 'Ouvrir le chat', 'close' => 'Fermer le chat', 'thinking' => 'Recherche d’une réponse…', 'searching' => 'Recherche dans la base de connaissances…', 'error' => 'Une erreur est survenue. Veuillez réessayer.', 'sources' => 'Sources', 'helpful' => 'Utile', 'notHelpful' => 'Pas utile', 'thanks' => 'Merci pour votre retour !', 'inputLabel' => 'Saisissez votre question', 'language' => 'Langue', 'lockedRefusal' => 'Cette question sort des informations dont dispose ce site, je n\'ai donc rien pour y répondre. Laissez votre adresse e-mail et un membre de l\'équipe vous répondra directement.', 'contact' => 'Laisser vos coordonnées', 'contactTitle' => 'Laissez vos coordonnées pour que nous puissions vous recontacter', 'contactIntro' => 'Facultatif. Seul le personnel de ce site les voit.', 'contactPhone' => 'Téléphone', 'contactEmail' => 'E-mail', 'contactConsent' => 'J’accepte que ce site me contacte au sujet de ma question.', 'contactSend' => 'Envoyer', 'contactDone' => 'Merci. Nous vous recontacterons.', 'contactMissing' => 'Indiquez un numéro de téléphone ou une adresse e-mail.', 'contactError' => 'Vos coordonnées n’ont pas pu être envoyées. Réessayez dans quelques minutes.', ), 'de' => array( 'send' => 'Senden', 'voiceStart' => 'Sprechen Sie Ihre Frage', 'voiceStop' => 'Zuhören beenden', 'attach' => 'Ein Bild senden', 'attachRemove' => 'Bild entfernen', 'attachFailed' => 'Dieses Bild konnte nicht vorbereitet werden. Bitte ein JPEG-, PNG- oder WebP-Bild verwenden.', 'attachNeeds' => 'Bitte schreiben Sie auch Ihre Frage — das Bild wird mit ihr gesendet.', 'open' => 'Chat öffnen', 'close' => 'Chat schließen', 'thinking' => 'Antwort wird gesucht…', 'searching' => 'Wissensdatenbank wird durchsucht…', 'error' => 'Es ist ein Fehler aufgetreten. Bitte erneut versuchen.', 'sources' => 'Quellen', 'helpful' => 'Hilfreich', 'notHelpful' => 'Nicht hilfreich', 'thanks' => 'Danke für Ihre Rückmeldung!', 'inputLabel' => 'Geben Sie Ihre Frage ein', 'language' => 'Sprache', 'lockedRefusal' => 'Diese Frage liegt außerhalb der Informationen dieser Website, daher habe ich keine Grundlage für eine Antwort. Bitte lassen Sie Ihre E-Mail-Adresse da, dann antwortet Ihnen ein Mitarbeiter direkt.', 'contact' => 'Kontaktdaten hinterlassen', 'contactTitle' => 'Hinterlassen Sie Ihre Kontaktdaten, damit wir uns melden können', 'contactIntro' => 'Freiwillig. Nur die Mitarbeiter dieser Website sehen sie.', 'contactPhone' => 'Telefon', 'contactEmail' => 'E-Mail', 'contactConsent' => 'Ich bin damit einverstanden, dass diese Website mich zu meiner Frage kontaktiert.', 'contactSend' => 'Senden', 'contactDone' => 'Danke. Wir melden uns bei Ihnen.', 'contactMissing' => 'Bitte hinterlassen Sie eine Telefonnummer oder eine E-Mail-Adresse.', 'contactError' => 'Ihre Daten konnten nicht gesendet werden. Bitte versuchen Sie es in einigen Minuten erneut.', ), 'es' => array( 'send' => 'Enviar', 'voiceStart' => 'Dicte su pregunta', 'voiceStop' => 'Dejar de escuchar', 'attach' => 'Enviar una imagen', 'attachRemove' => 'Quitar la imagen', 'attachFailed' => 'No se pudo preparar esta imagen. Utilice una imagen JPEG, PNG o WebP.', 'attachNeeds' => 'Escriba también su pregunta: la imagen se envía junto con ella.', 'open' => 'Abrir el chat', 'close' => 'Cerrar el chat', 'thinking' => 'Buscando una respuesta...', 'searching' => 'Buscando en la base de conocimiento...', 'error' => 'Se ha producido un error. Inténtalo de nuevo.', 'sources' => 'Fuentes', 'helpful' => 'Útil', 'notHelpful' => 'No es útil', 'thanks' => '¡Gracias por tu valoración!', 'inputLabel' => 'Escribe tu pregunta', 'language' => 'Idioma', 'lockedRefusal' => 'Esta pregunta queda fuera de la información de este sitio, así que no tengo datos para responderla. Deja tu correo y una persona del equipo te responderá directamente.', 'contact' => 'Dejar mis datos', 'contactTitle' => 'Deje sus datos para que podamos responderle', 'contactIntro' => 'Es opcional. Solo lo ve el personal de este sitio.', 'contactPhone' => 'Teléfono', 'contactEmail' => 'Correo electrónico', 'contactConsent' => 'Acepto que este sitio me contacte sobre mi consulta.', 'contactSend' => 'Enviar', 'contactDone' => 'Gracias. Nos pondremos en contacto.', 'contactMissing' => 'Deje un teléfono o un correo electrónico.', 'contactError' => 'No se pudieron enviar sus datos. Inténtelo de nuevo en unos minutos.', ), 'pt' => array( 'send' => 'Enviar', 'voiceStart' => 'Diga a sua pergunta', 'voiceStop' => 'Parar de ouvir', 'attach' => 'Enviar uma imagem', 'attachRemove' => 'Remover a imagem', 'attachFailed' => 'Não foi possível preparar esta imagem. Use uma imagem JPEG, PNG ou WebP.', 'attachNeeds' => 'Escreva também a sua pergunta — a imagem é enviada junto com ela.', 'open' => 'Abrir o chat', 'close' => 'Fechar o chat', 'thinking' => 'Procurando uma resposta...', 'searching' => 'Pesquisando na base de conhecimento...', 'error' => 'Ocorreu um erro. Tente novamente.', 'sources' => 'Fontes', 'helpful' => 'Útil', 'notHelpful' => 'Não foi útil', 'thanks' => 'Obrigado pelo seu comentário!', 'inputLabel' => 'Escreva a sua pergunta', 'language' => 'Idioma', 'lockedRefusal' => 'Esta pergunta está fora da informação deste site, por isso não tenho dados para responder. Deixe o seu e-mail e um membro da equipa responderá diretamente.', 'contact' => 'Deixar os meus dados', 'contactTitle' => 'Deixe os seus dados para podermos responder', 'contactIntro' => 'É opcional. Só a equipa deste site os vê.', 'contactPhone' => 'Telefone', 'contactEmail' => 'E-mail', 'contactConsent' => 'Aceito que este site me contacte sobre a minha pergunta.', 'contactSend' => 'Enviar', 'contactDone' => 'Obrigado. Entraremos em contacto.', 'contactMissing' => 'Deixe um telefone ou um e-mail.', 'contactError' => 'Não foi possível enviar os seus dados. Tente novamente em alguns minutos.', ), 'ru' => array( 'send' => 'Отправить', 'voiceStart' => 'Продиктуйте вопрос', 'voiceStop' => 'Остановить прослушивание', 'attach' => 'Отправить изображение', 'attachRemove' => 'Удалить изображение', 'attachFailed' => 'Не удалось подготовить это изображение. Используйте JPEG, PNG или WebP.', 'attachNeeds' => 'Напишите также ваш вопрос — изображение отправляется вместе с ним.', 'open' => 'Открыть чат', 'close' => 'Закрыть чат', 'thinking' => 'Ищем ответ…', 'searching' => 'Поиск в базе знаний…', 'error' => 'Произошла ошибка. Попробуйте ещё раз.', 'sources' => 'Источники', 'helpful' => 'Полезно', 'notHelpful' => 'Не полезно', 'thanks' => 'Спасибо за отзыв!', 'inputLabel' => 'Введите ваш вопрос', 'language' => 'Язык', 'lockedRefusal' => 'Этот вопрос выходит за рамки информации, которая есть на этом сайте, поэтому мне нечем на него ответить. Оставьте, пожалуйста, адрес почты, и сотрудник ответит вам напрямую.', 'contact' => 'Оставить контакты', 'contactTitle' => 'Оставьте контакты, чтобы мы могли связаться с вами', 'contactIntro' => 'Необязательно. Их видят только сотрудники этого сайта.', 'contactPhone' => 'Телефон', 'contactEmail' => 'Эл. почта', 'contactConsent' => 'Я согласен(-на), что этот сайт свяжется со мной по моему вопросу.', 'contactSend' => 'Отправить', 'contactDone' => 'Спасибо. Мы свяжемся с вами.', 'contactMissing' => 'Оставьте номер телефона или адрес эл. почты.', 'contactError' => 'Не удалось отправить данные. Попробуйте через несколько минут.', ), ); /** * The refusal sentence the knowledge lock answers with — and the note that shapes it. * * `lockedRefusal` is a REPLY, not chrome: it is what the visitor reads when the lock is on and * the knowledge base has nothing above the score floor. It lives here for the same reason the * hand-over sentences do — the visitor reads it, so it must follow the VISITOR'S language, * while the .po catalogue is loaded for the admin language (docs/08 muc 6.39, 6.45). * * The two notes are instructions to the model, one per situation the lock allows the model to * be called in. Keeping them beside the other instructions is what makes "add a language" a * table edit rather than a code change. * * @var array */ const LOCK_NOTES = array( 'vi' => array( 'strict' => 'CHỈ trả lời bằng thông tin trong phần DỮ LIỆU ở dưới. Tuyệt đối không dùng kiến thức bên ngoài, không suy đoán, không bổ sung nguồn khác. Nếu phần DỮ LIỆU không có câu trả lời, hãy nói rõ là chưa có thông tin và đề nghị khách để lại email.', 'smallTalk' => 'Tin nhắn này là một câu chào hỏi hoặc câu nói ngắn. Hãy đáp lại ngắn gọn và lịch sự, và TUYỆT ĐỐI không cung cấp thông tin thực tế nào về sản phẩm, giá, bảo hành hay chính sách. Mời khách hỏi về sản phẩm và dịch vụ của website này.', ), 'en' => array( 'strict' => 'Answer ONLY from the DATA section below. Do not use any outside knowledge, do not guess, and do not bring in other sources. If the DATA does not answer the question, say clearly that the information is not available and ask the visitor to leave an email address.', 'smallTalk' => 'This message is a greeting or a short remark. Reply briefly and politely, and do NOT provide any factual information about products, prices, warranties or policies. Invite the visitor to ask about this website\'s products and services.', ), 'zh' => array( 'strict' => '只根据下面的“数据”部分回答。绝对不要使用外部知识,不要猜测,也不要引入其他来源。如果“数据”中没有答案,请明确说明暂无相关信息,并请客户留下邮箱。', 'smallTalk' => '这条消息只是问候或简短寒暄。请简短、礼貌地回应,并且绝对不要提供任何关于产品、价格、保修或政策的事实信息。请邀请客户询问本网站的产品与服务。', ), 'ja' => array( 'strict' => '以下の「データ」セクションの情報だけに基づいて回答してください。外部の知識を使わず、推測せず、他の情報源にも触れないでください。「データ」に答えがない場合は、情報がない旨を明確に伝え、メールアドレスの記入をお願いしてください。', 'smallTalk' => 'このメッセージは挨拶または短い相づちです。短く丁寧に応答し、製品・価格・保証・ポリシーに関する事実情報は一切提供しないでください。このサイトの製品やサービスについて質問するよう案内してください。', ), 'ko' => array( 'strict' => '아래 "데이터" 섹션의 정보만으로 답변하세요. 외부 지식을 사용하지 말고, 추측하지 말고, 다른 출처를 언급하지 마세요. "데이터"에 답이 없으면 정보가 없음을 분명히 알리고 이메일 주소를 남겨 달라고 안내하세요.', 'smallTalk' => '이 메시지는 인사말이나 짧은 말입니다. 짧고 정중하게 응답하고, 제품·가격·보증·정책에 관한 사실 정보는 절대 제공하지 마세요. 이 웹사이트의 제품과 서비스에 대해 질문하도록 안내하세요.', ), 'th' => array( 'strict' => 'ตอบโดยใช้ข้อมูลในส่วน "ข้อมูล" ด้านล่างเท่านั้น ห้ามใช้ความรู้ภายนอก ห้ามเดา และห้ามอ้างแหล่งอื่น หากในส่วนข้อมูลไม่มีคำตอบ ให้แจ้งชัดว่าไม่มีข้อมูลและขอให้ลูกค้าฝากอีเมลไว้', 'smallTalk' => 'ข้อความนี้เป็นคำทักทายหรือคำพูดสั้น ๆ ให้ตอบสั้น ๆ อย่างสุภาพ และห้ามให้ข้อมูลข้อเท็จจริงเกี่ยวกับสินค้า ราคา การรับประกัน หรือนโยบายใด ๆ เชิญชวนให้ลูกค้าถามเกี่ยวกับสินค้าและบริการของเว็บไซต์นี้', ), 'id' => array( 'strict' => 'Jawab HANYA berdasarkan bagian DATA di bawah ini. Jangan gunakan pengetahuan luar, jangan menebak, dan jangan mengutip sumber lain. Jika DATA tidak memuat jawabannya, katakan dengan jelas bahwa informasinya belum tersedia dan minta pelanggan meninggalkan email.', 'smallTalk' => 'Pesan ini adalah sapaan atau ucapan singkat. Jawablah dengan singkat dan sopan, dan JANGAN memberikan informasi faktual apa pun tentang produk, harga, garansi, atau kebijakan. Ajak pelanggan bertanya tentang produk dan layanan situs ini.', ), 'fr' => array( 'strict' => 'Répondez UNIQUEMENT à partir de la section DONNÉES ci-dessous. N\'utilisez aucune connaissance extérieure, ne devinez pas et ne citez pas d\'autres sources. Si les DONNÉES ne contiennent pas la réponse, dites clairement que l\'information n\'est pas disponible et invitez le client à laisser une adresse e-mail.', 'smallTalk' => 'Ce message est une salutation ou une remarque courte. Répondez brièvement et poliment, et ne fournissez AUCUNE information factuelle sur les produits, les prix, les garanties ou les politiques. Invitez le client à poser une question sur les produits et services de ce site.', ), 'de' => array( 'strict' => 'Antworten Sie AUSSCHLIESSLICH auf Basis des Abschnitts DATEN unten. Verwenden Sie kein außenstehendes Wissen, raten Sie nicht und nennen Sie keine anderen Quellen. Wenn die DATEN die Frage nicht beantworten, sagen Sie klar, dass die Information nicht vorliegt, und bitten Sie den Kunden, eine E-Mail-Adresse zu hinterlassen.', 'smallTalk' => 'Diese Nachricht ist eine Begrüßung oder eine kurze Bemerkung. Antworten Sie kurz und höflich und geben Sie KEINE sachlichen Informationen zu Produkten, Preisen, Garantien oder Richtlinien. Bitten Sie den Kunden, nach Produkten und Leistungen dieser Website zu fragen.', ), 'es' => array( 'strict' => 'Responde ÚNICAMENTE con la información de la sección DATOS de abajo. No uses conocimiento externo, no adivines ni cites otras fuentes. Si los DATOS no contienen la respuesta, di claramente que no hay información y pide al cliente que deje un correo electrónico.', 'smallTalk' => 'Este mensaje es un saludo o un comentario breve. Responde de forma breve y cortés, y NO proporciones información factual sobre productos, precios, garantías o políticas. Invita al cliente a preguntar sobre los productos y servicios de este sitio.', ), 'pt' => array( 'strict' => 'Responda APENAS com base na secção DADOS abaixo. Não use conhecimento externo, não adivinhe e não cite outras fontes. Se os DADOS não tiverem a resposta, diga claramente que a informação não está disponível e peça ao cliente que deixe um e-mail.', 'smallTalk' => 'Esta mensagem é uma saudação ou um comentário curto. Responda de forma breve e educada e NÃO forneça informação factual sobre produtos, preços, garantias ou políticas. Convide o cliente a perguntar sobre os produtos e serviços deste site.', ), 'ru' => array( 'strict' => 'Отвечайте ТОЛЬКО на основе раздела ДАННЫЕ ниже. Не используйте посторонние знания, не догадывайтесь и не ссылайтесь на другие источники. Если в ДАННЫХ нет ответа, прямо сообщите, что информации нет, и попросите клиента оставить адрес электронной почты.', 'smallTalk' => 'Это сообщение — приветствие или короткая реплика. Ответьте кратко и вежливо и НЕ сообщайте никаких фактических сведений о товарах, ценах, гарантии или правилах. Предложите клиенту спросить о товарах и услугах этого сайта.', ), ); /** * Settings. * * @var MYBOT_Settings */ protected $settings; /** * Constructor. * * @param MYBOT_Settings $settings Settings. */ public function __construct( $settings ) { $this->settings = $settings; } /** * List of supported languages. * * @return array */ public static function supported() { return self::SUPPORTED; } /** * Languages the ADMIN interface can actually be shown in — read from the files that exist. * * The list is DISCOVERED, not written by hand, and that is the whole point: a hand-written list * of interface languages goes stale the moment somebody drops in a translation, and its failure * is silent — the option is offered, the owner picks it, and nothing on the screen changes. * Anything with a `languages/aihouse-.mo` next to it can be chosen; nothing else can. * * The prefix is the TEXT DOMAIN, and the text domain is the WordPress.org slug — the directory requires * the two to be the same string, and a mismatch there is `textdomain_mismatch`, a hard error at submission. * * English is always offered first: it is the source language of every string in the plugin, so it * needs no file, and it is the honest answer for "show me the original wording". * * @return array locale => label, English first. */ public static function admin_locale_options() { $options = array( 'en_US' => 'English' ); $files = glob( MYBOT_PLUGIN_DIR . 'languages/aihouse-*.mo' ); foreach ( (array) $files as $file ) { $locale = substr( basename( $file, '.mo' ), strlen( 'aihouse-' ) ); if ( '' === $locale || isset( $options[ $locale ] ) ) { continue; } $options[ $locale ] = self::locale_label( $locale ); } return $options; } /** * A readable label for a locale, e.g. "Français (fr_FR)". * * Uses the native names the chat already carries when the language matches, because those are * the names a person recognises; an unknown locale is shown as itself rather than hidden. * * @param string $locale WordPress locale, e.g. vi or fr_FR. * @return string */ protected static function locale_label( $locale ) { $code = strtolower( substr( $locale, 0, 2 ) ); if ( isset( self::SUPPORTED[ $code ] ) ) { return self::SUPPORTED[ $code ] . ' (' . $locale . ')'; } return $locale; } /** * Currently enabled languages. * * Always returns at least one language so the chatbot never ends up with no * language at all. * * @return array List of language codes. */ public function enabled() { $enabled = $this->settings->get( 'languages', array( 'vi', 'en' ) ); if ( ! is_array( $enabled ) ) { $enabled = array(); } $enabled = array_values( array_intersect( array_map( 'sanitize_key', $enabled ), array_keys( self::SUPPORTED ) ) ); if ( empty( $enabled ) ) { $enabled = array( 'en' ); } return $enabled; } /** * Default language code. * * @return string */ public function default_code() { $enabled = $this->enabled(); $default = sanitize_key( (string) $this->settings->get( 'default_language', 'en' ) ); return in_array( $default, $enabled, true ) ? $default : $enabled[0]; } /** * Whether the language selector is shown. * * @return bool */ public function show_switch() { return ! empty( $this->settings->get( 'show_language_switch', 1 ) ) && count( $this->enabled() ) > 1; } /** * The language a visitor's own message is written in — but only when that is beyond doubt. * * Why this exists at all, when the instruction to the model already makes the *answer* follow the * visitor (see INSTRUCTIONS): the model writes the reply, but it does not write the interface. The * widget's buttons, its placeholder and the fallback sentence used when the LLM is unreachable all * come from the language code, so without this a visitor who writes Vietnamese into an EN widget * would read a Vietnamese answer inside an English frame — and an English "something went wrong" * if the provider happened to be down. * * The rule is deliberately CONSERVATIVE: it answers only "beyond doubt", and returns '' otherwise. * A wrong switch is worse than no switch, because it changes the language of everything the * visitor reads next, and nothing on the screen says a guess was made: * * - Scripts that identify their language on their own: Kana (Japanese), Hangul (Korean), Thai and * Cyrillic. Those are asked FIRST, and the order carries the meaning — Japanese and Korean * are written with Han ideographs beside their own script, so an ordinary Japanese sentence * ("保証期間はどのくらいですか") matches Han as well. Asking Han first answered "Chinese" for * normal Japanese, which repainted a Japanese visitor's widget into a script they cannot * read, and — when Chinese was not enabled — refused to move at all for a message that was * never in doubt (docs/08 muc 6.36). * - Han last among those, and only as the answer for Han-only text. Han is SHARED: it is Chinese, * it is Japanese kanji, it is Korean hanja. Han-only text is therefore the one case no * character test can settle between Chinese and Japanese, and the honest reading is "this is * Chinese, or it is Japanese and we cannot tell" — see the note on the table below. * - Vietnamese, by the characters only Vietnamese has. Not by `ă â ê ô đ` (Romanian, French, * Croatian share those) and not by `ã õ` (those are Portuguese): the set below is the one * Vietnamese precomposed vowels with a tone mark that no other Latin-script language uses. * - NOT by the unaccented form. "toi can tim hieu bao hanh" is Vietnamese, and no character * test can prove it — French, Spanish and Indonesian text without accents looks similar * enough that a guess would be a coin flip. That message still gets a Vietnamese ANSWER, * because identifying language is exactly what the model is better at (INSTRUCTIONS); it just * does not get to repaint the visitor's interface on the strength of a hunch. * * @param string $text Visitor's message. * @return string An enabled language code, or '' when the message gives no decisive evidence. */ public function detect( $text ) { $text = (string) $text; if ( '' === trim( $text ) ) { return ''; } /* * Order is the rule, not a style choice (docs/08 muc 6.36). * * Kana and Hangul are asked BEFORE Han because a Japanese or Korean message normally carries * Han ideographs of its own, so Han matches it too — and whichever pattern is asked first * decides. Han is the shared script; the other four are not shared, which is exactly why they * get the first word. `preg_match` takes the first match, so this table is read top to bottom. */ $scripts = array( '/[\p{Hiragana}\p{Katakana}]/u' => 'ja', '/\p{Hangul}/u' => 'ko', '/\p{Han}/u' => 'zh', '/\p{Thai}/u' => 'th', '/\p{Cyrillic}/u' => 'ru', ); foreach ( $scripts as $pattern => $code ) { if ( preg_match( $pattern, $text ) ) { return $this->is_enabled( $code ) ? $code : ''; } } if ( preg_match( '/[ơưạảậặẹẻẽệếềểễỉịọỏốồổỗộớờởỡợụủứừửữỳỵỷỹự]/u', $text ) ) { return $this->is_enabled( 'vi' ) ? 'vi' : ''; } return ''; } /** * Whether this language is currently enabled. * * @param string $code Language code. * @return bool */ public function is_enabled( $code ) { return in_array( sanitize_key( $code ), $this->enabled(), true ); } /** * Normalize the language sent by the client. * * Never trust the client: only accept a code from the enabled list, otherwise * fall back to the default language. * * @param string $requested Code sent by the client. * @return string */ public function resolve( $requested ) { $requested = sanitize_key( (string) $requested ); return $this->is_enabled( $requested ) ? $requested : $this->default_code(); } /** * Get a content string for a language. * * Lookup order: the requested language, the default language, and finally the * legacy top-level field for compatibility with settings saved before * multilingual support was added. * * @param string $code Language code. * @param string $key Field name: bot_name, welcome, placeholder, system_prompt. * @return string */ public function string( $code, $key ) { $map = $this->settings->get( 'lang_strings', array() ); $map = is_array( $map ) ? $map : array(); $code = sanitize_key( $code ); $default = $this->default_code(); foreach ( array( $code, $default ) as $candidate ) { if ( isset( $map[ $candidate ][ $key ] ) && '' !== trim( (string) $map[ $candidate ][ $key ] ) ) { return (string) $map[ $candidate ][ $key ]; } } return (string) $this->settings->get( $key, '' ); } /** * The instruction the knowledge lock adds to the system prompt. * * Two situations, and the lock uses exactly one of them: `strict` when the knowledge base * returned content and the model must stay inside it, `smallTalk` when the message is so short * that the knowledge base will never match it (the owner chose to keep greetings answered — * docs/08 muc 6.45(d)). The second note is deliberately narrow: it may greet, and it may not * state a single fact. * * @param string $code Language code. * @param string $key strict or smallTalk. * @return string */ public function lock_note( $code, $key = 'strict' ) { $code = sanitize_key( $code ); $key = ( 'smallTalk' === $key ) ? 'smallTalk' : 'strict'; if ( isset( self::LOCK_NOTES[ $code ][ $key ] ) ) { return (string) self::LOCK_NOTES[ $code ][ $key ]; } return (string) self::LOCK_NOTES['en'][ $key ]; } /** * The refusal sentence a locked site answers with when the knowledge base has nothing. * * @param string $code Language code. * @return string */ public static function locked_refusal( $code ) { $strings = self::ui_strings( $code ); return isset( $strings['lockedRefusal'] ) ? (string) $strings['lockedRefusal'] : ''; } /** * Instruction that forces the LLM to answer in the correct language. * * @param string $code Language code. * @return string */ public function instruction( $code ) { $code = sanitize_key( $code ); $custom = $this->string( $code, 'instruction' ); if ( '' !== trim( $custom ) ) { return $custom; } return isset( self::INSTRUCTIONS[ $code ] ) ? self::INSTRUCTIONS[ $code ] : ''; } /** * Per-language RAG note. * * @param string $code Language code. * @return string */ public function rag_note( $code ) { $code = sanitize_key( $code ); return isset( self::RAG_NOTES[ $code ] ) ? self::RAG_NOTES[ $code ] : self::RAG_NOTES['en']; } /** * Short label for the selector button. * * @param string $code Language code. * @return string */ public static function short_label( $code ) { $code = sanitize_key( $code ); if ( isset( self::SHORT[ $code ] ) ) { return self::SHORT[ $code ]; } return strtoupper( substr( $code, 0, 2 ) ); } /** * List of enabled languages with labels, used by the front-end selector. * * @return array */ public function switch_options() { $options = array(); foreach ( $this->enabled() as $code ) { $options[] = array( 'code' => $code, 'short' => self::short_label( $code ), 'label' => self::SUPPORTED[ $code ], ); } return $options; } /** * Chat widget interface strings for each language. * * Sent once on page load so visitors can switch language without another server * call. * * @return array */ public function widget_strings() { $strings = array(); foreach ( $this->enabled() as $code ) { $strings[ $code ] = array( 'botName' => $this->string( $code, 'bot_name' ), 'welcome' => $this->string( $code, 'welcome' ), 'placeholder' => $this->string( $code, 'placeholder' ), /* * The floating button's own words (1.9.4). They are sent with the rest so that * switching language moves them too — the label sits on the button and the invitation * beside it, both outside the chat window, which is precisely where a forgotten string * hides (docs/08 muc 6.39). */ 'launcherLabel' => $this->string( $code, 'launcher_label' ), 'teaserText' => $this->string( $code, 'teaser_text' ), ); /* * Three sources, one array: the owner's content, the widget's own interface strings, and * the hand-over form. The visitor's choice has to move ALL of them — sending only the * first was the bug in docs/08 muc 6.38. */ $strings[ $code ] = array_merge( $strings[ $code ], self::ui_strings( $code ), self::support_strings( $code ) ); } return $strings; } /** * Hand-over-to-a-human strings, per language. * * Kept beside the widget strings rather than in the .po files on purpose: these sentences are * read by visitors, and the .po catalog is only loaded for the admin language. A language * without an entry falls back to English, so adding one is a copy-and-translate job with no * code change. * * @param string $code Language code. * @return array */ public static function support_strings( $code ) { $strings = array( 'vi' => array( 'supportTitle' => 'Câu hỏi này cần nhân viên trả lời', 'supportIntro' => 'Rất tiếc, tôi chưa có thông tin để trả lời câu hỏi này. Để lại email, nhân viên của chúng tôi sẽ trả lời bạn trực tiếp.', 'supportName' => 'Tên của bạn', 'supportEmail' => 'Email (bắt buộc)', 'supportPhone' => 'Số điện thoại (không bắt buộc)', 'supportNote' => 'Bạn muốn bổ sung thêm điều gì? (không bắt buộc)', 'supportSend' => 'Gửi câu hỏi cho nhân viên', 'supportSending' => 'Đang gửi...', 'supportDone' => 'Cảm ơn bạn. Câu hỏi đã được gửi tới nhân viên và chúng tôi sẽ trả lời qua email.', 'supportMissing' => 'Vui lòng điền email để chúng tôi trả lời được cho bạn.', 'supportError' => 'Không gửi được câu hỏi. Vui lòng thử lại sau ít phút.', 'supportLater' => 'Để sau', ), 'en' => array( 'supportTitle' => 'This question needs a person', 'supportIntro' => 'I am sorry, I do not have the information to answer this one. Leave your email and someone from our team will reply to you directly.', 'supportName' => 'Your name', 'supportEmail' => 'Email (required)', 'supportPhone' => 'Phone (optional)', 'supportNote' => 'Anything you would like to add? (optional)', 'supportSend' => 'Send my question to the team', 'supportSending' => 'Sending...', 'supportDone' => 'Thank you. Your question has been sent to our team, and we will reply by email.', 'supportMissing' => 'Please enter an email address so we can reply to you.', 'supportError' => 'The question could not be sent. Please try again in a few minutes.', 'supportLater' => 'Not now', ), /* * The other ten languages of the widget, added in 1.9.0 (docs/08 muc 6.38). * * Before this, a visitor who picked Japanese got the hand-over form in ENGLISH — the * form is the last screen before a lost visitor leaves, and it was the one screen that * only existed in two languages. These are first-pass translations, written to be * correct and polite rather than idiomatic; a native review would improve the phrasing * without changing a line of code, which is exactly the property the table is for. */ 'zh' => array( 'supportTitle' => '这个问题需要人工处理', 'supportIntro' => '抱歉,我没有可以回答这个问题的信息。请留下您的邮箱,我们的同事会直接回复您。', 'supportName' => '您的称呼', 'supportEmail' => '邮箱(必填)', 'supportPhone' => '电话(选填)', 'supportNote' => '您想补充什么吗?(选填)', 'supportSend' => '把问题发给客服', 'supportSending' => '正在发送...', 'supportDone' => '谢谢。您的问题已发送给我们的团队,我们会通过邮件回复您。', 'supportMissing' => '请填写邮箱,以便我们回复您。', 'supportError' => '问题发送失败。请过几分钟再试。', 'supportLater' => '暂不', ), 'ja' => array( 'supportTitle' => 'この質問は担当者が必要です', 'supportIntro' => '申し訳ありません。この質問にお答えできる情報がありません。メールアドレスをご記入いただければ、担当者より直接ご連絡いたします。', 'supportName' => 'お名前', 'supportEmail' => 'メールアドレス(必須)', 'supportPhone' => '電話番号(任意)', 'supportNote' => '補足があればご記入ください(任意)', 'supportSend' => '担当者に質問を送る', 'supportSending' => '送信中...', 'supportDone' => 'ありがとうございます。ご質問を担当チームに送信しました。メールでご返信いたします。', 'supportMissing' => 'ご返信できるよう、メールアドレスをご記入ください。', 'supportError' => '送信できませんでした。数分後にもう一度お試しください。', 'supportLater' => '後で', ), 'ko' => array( 'supportTitle' => '이 질문은 상담원이 필요합니다', 'supportIntro' => '죄송합니다. 이 질문에 답할 정보가 없습니다. 이메일을 남겨 주시면 담당자가 직접 답변드리겠습니다.', 'supportName' => '이름', 'supportEmail' => '이메일 (필수)', 'supportPhone' => '전화번호 (선택)', 'supportNote' => '추가로 남기실 내용이 있나요? (선택)', 'supportSend' => '상담원에게 질문 보내기', 'supportSending' => '보내는 중...', 'supportDone' => '감사합니다. 질문이 담당 팀에 전달되었으며 이메일로 답변드리겠습니다.', 'supportMissing' => '답변을 드릴 수 있도록 이메일 주소를 입력해 주세요.', 'supportError' => '질문을 보내지 못했습니다. 몇 분 후 다시 시도해 주세요.', 'supportLater' => '나중에', ), 'th' => array( 'supportTitle' => 'คำถามนี้ต้องให้เจ้าหน้าที่ตอบ', 'supportIntro' => 'ขออภัย ฉันไม่มีข้อมูลสำหรับตอบคำถามนี้ กรุณาฝากอีเมลไว้ เจ้าหน้าที่ของเราจะตอบคุณโดยตรง', 'supportName' => 'ชื่อของคุณ', 'supportEmail' => 'อีเมล (จำเป็น)', 'supportPhone' => 'เบอร์โทร (ไม่บังคับ)', 'supportNote' => 'มีอะไรต้องการเพิ่มเติมไหม? (ไม่บังคับ)', 'supportSend' => 'ส่งคำถามถึงเจ้าหน้าที่', 'supportSending' => 'กำลังส่ง...', 'supportDone' => 'ขอบคุณ คำถามของคุณถูกส่งถึงทีมแล้ว เราจะตอบกลับทางอีเมล', 'supportMissing' => 'กรุณากรอกอีเมลเพื่อให้เราตอบกลับคุณได้', 'supportError' => 'ส่งคำถามไม่สำเร็จ กรุณาลองใหม่ในอีกสักครู่', 'supportLater' => 'ไว้ทีหลัง', ), 'id' => array( 'supportTitle' => 'Pertanyaan ini perlu ditangani staf', 'supportIntro' => 'Maaf, saya tidak punya informasi untuk menjawab pertanyaan ini. Tinggalkan email Anda dan staf kami akan membalas langsung.', 'supportName' => 'Nama Anda', 'supportEmail' => 'Email (wajib)', 'supportPhone' => 'Telepon (opsional)', 'supportNote' => 'Ada yang ingin ditambahkan? (opsional)', 'supportSend' => 'Kirim pertanyaan ke staf', 'supportSending' => 'Mengirim...', 'supportDone' => 'Terima kasih. Pertanyaan Anda sudah dikirim ke tim kami dan akan kami balas lewat email.', 'supportMissing' => 'Mohon isi alamat email agar kami bisa membalas.', 'supportError' => 'Pertanyaan gagal dikirim. Coba lagi beberapa menit lagi.', 'supportLater' => 'Nanti saja', ), 'fr' => array( 'supportTitle' => 'Cette question nécessite un conseiller', 'supportIntro' => 'Je suis désolé, je n’ai pas les informations pour répondre à cette question. Laissez votre e-mail et un membre de notre équipe vous répondra directement.', 'supportName' => 'Votre nom', 'supportEmail' => 'E-mail (obligatoire)', 'supportPhone' => 'Téléphone (facultatif)', 'supportNote' => 'Souhaitez-vous ajouter quelque chose ? (facultatif)', 'supportSend' => 'Envoyer ma question à l’équipe', 'supportSending' => 'Envoi…', 'supportDone' => 'Merci. Votre question a été transmise à notre équipe et nous vous répondrons par e-mail.', 'supportMissing' => 'Veuillez indiquer une adresse e-mail pour que nous puissions vous répondre.', 'supportError' => 'La question n’a pas pu être envoyée. Réessayez dans quelques minutes.', 'supportLater' => 'Plus tard', ), 'de' => array( 'supportTitle' => 'Diese Frage braucht einen Mitarbeiter', 'supportIntro' => 'Es tut mir leid, ich habe keine Informationen, um diese Frage zu beantworten. Lassen Sie Ihre E-Mail-Adresse da, dann antwortet Ihnen jemand aus unserem Team direkt.', 'supportName' => 'Ihr Name', 'supportEmail' => 'E-Mail (erforderlich)', 'supportPhone' => 'Telefon (optional)', 'supportNote' => 'Möchten Sie etwas ergänzen? (optional)', 'supportSend' => 'Frage an das Team senden', 'supportSending' => 'Wird gesendet…', 'supportDone' => 'Danke. Ihre Frage wurde an unser Team gesendet, wir antworten per E-Mail.', 'supportMissing' => 'Bitte geben Sie eine E-Mail-Adresse an, damit wir antworten können.', 'supportError' => 'Die Frage konnte nicht gesendet werden. Bitte versuchen Sie es in einigen Minuten erneut.', 'supportLater' => 'Später', ), 'es' => array( 'supportTitle' => 'Esta pregunta necesita a una persona', 'supportIntro' => 'Lo siento, no tengo la información para responder a esta pregunta. Deja tu correo y alguien de nuestro equipo te responderá directamente.', 'supportName' => 'Tu nombre', 'supportEmail' => 'Correo electrónico (obligatorio)', 'supportPhone' => 'Teléfono (opcional)', 'supportNote' => '¿Quieres añadir algo? (opcional)', 'supportSend' => 'Enviar mi pregunta al equipo', 'supportSending' => 'Enviando...', 'supportDone' => 'Gracias. Tu pregunta se ha enviado a nuestro equipo y te responderemos por correo.', 'supportMissing' => 'Introduce un correo electrónico para que podamos responderte.', 'supportError' => 'No se pudo enviar la pregunta. Inténtalo de nuevo en unos minutos.', 'supportLater' => 'Ahora no', ), 'pt' => array( 'supportTitle' => 'Esta pergunta precisa de uma pessoa', 'supportIntro' => 'Desculpe, não tenho a informação para responder a esta pergunta. Deixe o seu e-mail e alguém da nossa equipa responderá diretamente.', 'supportName' => 'O seu nome', 'supportEmail' => 'E-mail (obrigatório)', 'supportPhone' => 'Telefone (opcional)', 'supportNote' => 'Quer acrescentar algo? (opcional)', 'supportSend' => 'Enviar a minha pergunta à equipa', 'supportSending' => 'A enviar...', 'supportDone' => 'Obrigado. A sua pergunta foi enviada à nossa equipa e responderemos por e-mail.', 'supportMissing' => 'Indique um e-mail para que possamos responder.', 'supportError' => 'Não foi possível enviar a pergunta. Tente novamente dentro de alguns minutos.', 'supportLater' => 'Agora não', ), 'ru' => array( 'supportTitle' => 'На этот вопрос должен ответить сотрудник', 'supportIntro' => 'Извините, у меня нет информации, чтобы ответить на этот вопрос. Оставьте свою почту, и сотрудник нашей команды ответит вам напрямую.', 'supportName' => 'Ваше имя', 'supportEmail' => 'Электронная почта (обязательно)', 'supportPhone' => 'Телефон (необязательно)', 'supportNote' => 'Хотите что-то добавить? (необязательно)', 'supportSend' => 'Отправить вопрос сотруднику', 'supportSending' => 'Отправляем…', 'supportDone' => 'Спасибо. Ваш вопрос отправлен нашей команде, мы ответим по электронной почте.', 'supportMissing' => 'Укажите адрес электронной почты, чтобы мы могли ответить.', 'supportError' => 'Не удалось отправить вопрос. Попробуйте ещё раз через несколько минут.', 'supportLater' => 'Не сейчас', ), ); /* * English is the safety net, and it is a PER-LINE net: a language missing one sentence falls * back for that sentence rather than showing nothing. Until 1.9.0 a language with no entry at * all produced an EMPTY array — which is how ten of the twelve languages ended up opening an * entirely English form (docs/08 muc 6.38). */ $merged = $strings['en']; $own = isset( $strings[ $code ] ) ? (array) $strings[ $code ] : array(); foreach ( $own as $key => $value ) { if ( '' !== trim( (string) $value ) ) { $merged[ $key ] = (string) $value; } } /** * Filters the hand-over strings for one language. * * @param array $strings Strings for this language, English as the fallback. * @param string $code Language code. */ return (array) apply_filters( 'mybot_support_strings', $merged, $code ); } /** * The widget's own interface strings for one language. * * These are the buttons and status lines the visitor reads around the conversation — Send, the two * "working on it" lines, the Sources heading, the rating buttons and the open/close labels. They * are resolved per chat language, with English as a per-line fallback, for the reason the class * comment on `UI_STRINGS` gives: they used to follow the SITE locale, so a visitor reading a * Japanese answer saw English buttons (docs/08 muc 6.38). * * @param string $code Language code. * @return array */ public static function ui_strings( $code ) { $code = sanitize_key( $code ); $merged = self::UI_STRINGS['en']; $own = isset( self::UI_STRINGS[ $code ] ) ? (array) self::UI_STRINGS[ $code ] : array(); foreach ( $own as $key => $value ) { if ( '' !== trim( (string) $value ) ) { $merged[ $key ] = (string) $value; } } /** * Filters the widget interface strings for one language. * * @param array $strings Strings for this language, English as the fallback. * @param string $code Language code. */ return (array) apply_filters( 'mybot_ui_strings', $merged, $code ); } }