2026-05-25 16:45:33 +08:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const postI18nColsSQL = `
|
|
|
|
|
COALESCE(p.text_zh,''), COALESCE(p.text_en,''), COALESCE(p.text_ja,''),
|
|
|
|
|
COALESCE(p.text_ko,''), COALESCE(p.text_vi,''), COALESCE(p.text_id,''), COALESCE(p.text_ms,'')`
|
|
|
|
|
|
|
|
|
|
const maxPostTextLen = 32768
|
|
|
|
|
const maxPostAttachments = 20
|
|
|
|
|
|
|
|
|
|
var postLinkRe = regexp.MustCompile(`https?://`)
|
|
|
|
|
|
|
|
|
|
type postTextI18n struct {
|
|
|
|
|
TextZh, TextEn, TextJa, TextKo, TextVi, TextId, TextMs string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type postLocalePayload struct {
|
|
|
|
|
Text string `json:"text"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 11:12:22 +08:00
|
|
|
// requestLangCode is the normalized locale from ?lang= or Accept-Language (zh, en, ja, …).
|
|
|
|
|
func requestLangCode(r *http.Request) string {
|
|
|
|
|
raw := strings.TrimSpace(r.URL.Query().Get("lang"))
|
|
|
|
|
if raw == "" {
|
|
|
|
|
raw = r.Header.Get("Accept-Language")
|
|
|
|
|
}
|
|
|
|
|
raw = strings.TrimSpace(strings.Split(raw, ",")[0])
|
|
|
|
|
if raw == "" {
|
|
|
|
|
return "zh"
|
|
|
|
|
}
|
|
|
|
|
return translateNormalizePostLang(raw)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 17:58:16 +08:00
|
|
|
// textForLang returns body text stored for the post's source language only.
|
|
|
|
|
func (t postTextI18n) textForLang(lang string) string {
|
|
|
|
|
switch translateNormalizePostLang(lang) {
|
|
|
|
|
case "en":
|
|
|
|
|
return strings.TrimSpace(t.TextEn)
|
|
|
|
|
case "ja":
|
|
|
|
|
return strings.TrimSpace(t.TextJa)
|
|
|
|
|
case "ko":
|
|
|
|
|
return strings.TrimSpace(t.TextKo)
|
|
|
|
|
case "vi":
|
|
|
|
|
return strings.TrimSpace(t.TextVi)
|
|
|
|
|
case "id":
|
|
|
|
|
return strings.TrimSpace(t.TextId)
|
|
|
|
|
case "ms":
|
|
|
|
|
return strings.TrimSpace(t.TextMs)
|
|
|
|
|
default:
|
|
|
|
|
return strings.TrimSpace(t.TextZh)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func postTextI18nFromSingle(lang, text string) postTextI18n {
|
|
|
|
|
lang = translateNormalizePostLang(lang)
|
|
|
|
|
text = strings.TrimSpace(text)
|
|
|
|
|
var t postTextI18n
|
|
|
|
|
switch lang {
|
|
|
|
|
case "en":
|
|
|
|
|
t.TextEn = text
|
|
|
|
|
case "ja":
|
|
|
|
|
t.TextJa = text
|
|
|
|
|
case "ko":
|
|
|
|
|
t.TextKo = text
|
|
|
|
|
case "vi":
|
|
|
|
|
t.TextVi = text
|
|
|
|
|
case "id":
|
|
|
|
|
t.TextId = text
|
|
|
|
|
case "ms":
|
|
|
|
|
t.TextMs = text
|
|
|
|
|
default:
|
|
|
|
|
t.TextZh = text
|
|
|
|
|
}
|
|
|
|
|
return t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func normalizePostSourceLang(lang string) string {
|
|
|
|
|
lang = translateNormalizePostLang(strings.TrimSpace(lang))
|
|
|
|
|
switch lang {
|
|
|
|
|
case "zh", "en", "ja", "ko", "vi", "id", "ms":
|
|
|
|
|
return lang
|
|
|
|
|
default:
|
|
|
|
|
return "zh"
|
|
|
|
|
}
|
2026-05-25 16:45:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t postTextI18n) anyNonEmpty() bool {
|
|
|
|
|
return strings.TrimSpace(t.TextZh) != "" ||
|
|
|
|
|
strings.TrimSpace(t.TextEn) != "" ||
|
|
|
|
|
strings.TrimSpace(t.TextJa) != "" ||
|
|
|
|
|
strings.TrimSpace(t.TextKo) != "" ||
|
|
|
|
|
strings.TrimSpace(t.TextVi) != "" ||
|
|
|
|
|
strings.TrimSpace(t.TextId) != "" ||
|
|
|
|
|
strings.TrimSpace(t.TextMs) != ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t postTextI18n) legacyPrimary() string {
|
|
|
|
|
if strings.TrimSpace(t.TextZh) != "" {
|
|
|
|
|
return strings.TrimSpace(t.TextZh)
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(t.TextEn) != "" {
|
|
|
|
|
return strings.TrimSpace(t.TextEn)
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scanPostTextI18n(zh, en, ja, ko, vi, id, ms string) postTextI18n {
|
|
|
|
|
return postTextI18n{TextZh: zh, TextEn: en, TextJa: ja, TextKo: ko, TextVi: vi, TextId: id, TextMs: ms}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 23:43:55 +08:00
|
|
|
func (t postTextI18n) toLocalizations() map[string]postLocalePayload {
|
|
|
|
|
return map[string]postLocalePayload{
|
|
|
|
|
"zh": {Text: t.TextZh},
|
|
|
|
|
"en": {Text: t.TextEn},
|
|
|
|
|
"ja": {Text: t.TextJa},
|
|
|
|
|
"ko": {Text: t.TextKo},
|
|
|
|
|
"vi": {Text: t.TextVi},
|
|
|
|
|
"id": {Text: t.TextId},
|
|
|
|
|
"ms": {Text: t.TextMs},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:45:33 +08:00
|
|
|
func postTextHasLink(text string) bool {
|
|
|
|
|
return postLinkRe.MatchString(text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func truncatePostTexts(t postTextI18n) postTextI18n {
|
|
|
|
|
tr := func(s string) string {
|
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
|
if len(s) > maxPostTextLen {
|
|
|
|
|
return s[:maxPostTextLen]
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
t.TextZh = tr(t.TextZh)
|
|
|
|
|
t.TextEn = tr(t.TextEn)
|
|
|
|
|
t.TextJa = tr(t.TextJa)
|
|
|
|
|
t.TextKo = tr(t.TextKo)
|
|
|
|
|
t.TextVi = tr(t.TextVi)
|
|
|
|
|
t.TextId = tr(t.TextId)
|
|
|
|
|
t.TextMs = tr(t.TextMs)
|
|
|
|
|
return t
|
|
|
|
|
}
|