e884bb61cb
# fix: avoid `topic` fallback for non-Latin titles via pragmatic ASCII transliteration > **Scope update (in response to review):** this PR is intentionally broader than its original "Arabic-only" framing. The implementation changes URL slug generation for **every non-Latin, non-CJK script** that `slugify` previously stripped — see *Scope* below for the explicit list. The goal is *not* linguistically correct romanization; it is "avoid collapsing to `/topic` by producing a usable ASCII slug." ## What this PR is (and isn't) **Goal:** when a question title contains characters outside Basic Latin / Latin Extended / CJK Han, generate a URL slug that is a deterministic ASCII approximation instead of letting `slugify` strip everything and falling back to the literal `"topic"`. **Non-goal:** this is *not* a linguistically correct multi-language romanizer. The output is a machine-acceptable ASCII slug, not what a native speaker would choose. For example, `こんにちは` → `konnichiha` (not the more natural `kon'nichiwa`), `ไทย` → `aithy` (not `thai`). Treat the slug as an opaque, stable, indexable identifier — the path-after-`/questions/<id>/` is for SEO and shareability, the canonical reference is always the ID. ## The bug Pure non-Latin titles previously got stripped by `slugify.Slugify`, hit the empty-result fallback in `htmltext.UrlTitle`, and collapsed to the literal slug `"topic"`. On a live multilingual site, every Arabic / Thai / Japanese-hiragana / Korean / Hebrew / Cyrillic question ended up at `/questions/<id>/topic`. ## The fix `UrlTitle()` gets a `convertNonLatin` pre-step that mirrors the existing `convertChinese` pre-step pattern, using `github.com/mozillazg/go-unidecode` (same author as `go-pinyin` already in the repo, to minimise new-dep friction). ``` UrlTitle(title) → convertChinese(title) // pre-existing: Han-block → pinyin → convertNonLatin(title) // NEW: detect non-Latin letters → unidecode to ASCII → clearEmoji / slugify / url.QueryEscape / cutLongTitle (unchanged) ``` The non-Latin detector skips ASCII, Latin-1 Supplement, Latin Extended-A/B, and CJK Han. Inputs that hit none of those non-Latin letter categories short-circuit and return unchanged, so Latin-only and Chinese-only inputs remain byte-identical (pinned by tests). ## Scope — what scripts are affected This PR changes behavior for **any** title containing letters in scripts that `slugify` doesn't handle. Confirmed by tests in `pkg/htmltext/htmltext_test.go`: | Script | Example title | Before | After | | --- | --- | --- | --- | | Arabic | `كيف حالك` | `topic` | `kyf-hlk` | | Mixed Latin + Arabic | `مرحبا hello` | `hello` | `mrhb-hello` | | Thai | `ไทย ไทย` | `topic` | `aithy-aithy` | | Japanese hiragana | `こんにちは` | `topic` | `konnichiha` | | Korean | `안녕하세요` | `topic` | `annyeonghaseyo` | | Hebrew | `שלום עולם` | `topic` | `shlvm-vlm` | | Cyrillic | `Привет мир` | `topic` | `privet-mir` | **Unchanged:** | Case | Behavior | | --- | --- | | Pure Latin (`hello world`) | unchanged → `hello-world` | | Pure Chinese (`这是一个,标题,title`) | unchanged → `zhe-shi-yi-ge-biao-ti` (pinyin path) | | Japanese with Han-block kanji (`日本`) | unchanged → `ri-ben` (caught by pre-existing pinyin path; treated as Chinese reading, not Japanese — a pre-existing limitation, **not** introduced by this PR) | | Emoji only (`😂😂😂`) | unchanged → `topic` | | Empty / whitespace | unchanged → `topic` | ## Transliteration quality — explicit acknowledgement `go-unidecode` is a generic Unicode → ASCII approximation. It is **not** a per-language romanization library. Specifically: - It will pick *one* approximation per codepoint regardless of language context. `ใ` → `ai` (Thai romanization is `i` or `ai` depending on standard), `한` → `han`, `語` → `Yu` (Chinese pinyin reading even when used in Japanese), etc. - The result is *good enough* to be a stable, URL-safe, human-recognizable handle, but speakers of the source language will not consider it "correct." - It is deterministic, so the same title always produces the same slug — important since `url_title` is recomputed on every request. If maintainers prefer to scope this PR more narrowly (e.g. Arabic only, and reject Thai/Hebrew/Cyrillic/etc.), the detector in `containsNonLatin` can be tightened to specific Unicode blocks — but that means the other scripts continue to collapse to `topic`, which is the bug we're trying to fix. I'd argue the broader fix is preferable to a piecemeal one, but happy to narrow if you want. ## Live deployment / real-world verification This patch has been running in production on **[ask.namasoft.com](https://ask.namasoft.com)** (an Apache Answer instance we operate) since deployment, built directly from this branch via `docker compose build`. The site hosts Arabic-language questions, so the fix exercises the affected code path on every page load. Sample question URL on the deployed instance: > `https://ask.namasoft.com/questions/10010000000000115` The slug in the URL is the transliterated Arabic title rather than `topic`. No data migration was needed since `url_title` is computed on every request from `Title` and never persisted (see *Why this is safe to ship* below). ## Admin-configurable The transliteration is gated by a package-level `atomic.Bool` (default **on**, since the current behavior is objectively broken for affected users): - `htmltext.SetTransliterateNonLatin(enabled bool)` - `htmltext.IsTransliterateNonLatinEnabled() bool` This is deliberately the minimum surface needed to satisfy "the setting must be readable from `UrlTitle()`". A follow-up PR can add an admin UI section that calls `SetTransliterateNonLatin` on save and on startup, without having to re-plumb every `htmltext.UrlTitle` call site through `context.Context`. **Default choice — please confirm:** I picked **default-on** because the existing `topic` behavior is a bug for affected users. If you'd prefer default-off for strict backward compat on existing installs, flip the `init()` in `pkg/htmltext/htmltext.go` to `Store(false)` and surface the toggle as opt-in. ## Why this is safe to ship - `url_title` is **not** a persisted column. It's not on the `Question` entity in `internal/entity/question_entity.go`, no migration has ever added/dropped it, and every call site (`question_service.go`, `revision_service.go`, `vote_service.go`, search/report/review/rank/comment services, controllers, repos) recomputes it from `Title` at response-build time via `htmltext.UrlTitle(...)`. - That means the fix is read-only: existing rows light up with correct slugs on the next request, with no migration and no data rewrite. - Rollback is just redeploying the prior image; nothing on disk changes. ## Test coverage `pkg/htmltext/htmltext_test.go`: - **`TestUrlTitleTable`** — table-driven, one case per affected script (the full matrix above), plus: - `empty` → `topic` - `pure latin unchanged` → byte-identical to pre-fix - `pure chinese unchanged` → byte-identical to pre-fix (pins existing pinyin behavior) - `japanese kanji goes through pinyin path unchanged` → documents the pre-existing Han-block limitation - `emoji only falls back to topic` → unchanged - `long arabic truncates at cutLongTitle boundary` → exercises the 150-byte cap and UTF-8 boundary safety - **`TestUrlTitleTransliterationToggle`** — with the toggle off, non-Latin titles collapse to `topic` (pre-fix behavior); with it on, they transliterate. - Existing `TestUrlTitle` left untouched. Test plan for reviewers: - [ ] `go test ./pkg/htmltext/...` — all pass - [ ] Visit the live sample URL above and confirm slug is transliterated, not `topic` - [ ] Verify Chinese / Latin / emoji-only / empty behavior is byte-identical to `main` (covered by table tests) ## Out of scope (intentionally) - No admin UI / site setting plumbing in this PR — see *Admin-configurable* above. Happy to do the React `Non-Latin Languages Handling` admin page + `SiteType` + service / controller / migration in a follow-up if maintainers want it. - No change to the `"topic"` empty-result fallback. - No plugin interface for slug generation — mirrored the existing `convertChinese` pre-step pattern instead. - No per-language romanization library — this is an explicit non-goal; see *Transliteration quality* above. ## Issues / discussion I didn't find an existing upstream issue covering this — happy to be pointed at one if there is. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: LinkinStars <linkinstar@foxmail.com>
193 lines
9.0 KiB
Modula-2
193 lines
9.0 KiB
Modula-2
// Licensed to the Apache Software Foundation (ASF) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you under the Apache License, Version 2.0 (the
|
|
// "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
module github.com/apache/answer
|
|
|
|
go 1.24.0
|
|
|
|
require (
|
|
github.com/Machiel/slugify v1.0.1
|
|
github.com/Masterminds/semver/v3 v3.3.0
|
|
github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267
|
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
|
github.com/bwmarrin/snowflake v0.3.0
|
|
github.com/disintegration/imaging v1.6.2
|
|
github.com/gin-gonic/gin v1.10.0
|
|
github.com/go-playground/locales v0.14.1
|
|
github.com/go-playground/universal-translator v0.18.1
|
|
github.com/go-playground/validator/v10 v10.22.1
|
|
github.com/go-resty/resty/v2 v2.17.1
|
|
github.com/go-sql-driver/mysql v1.8.1
|
|
github.com/goccy/go-json v0.10.3
|
|
github.com/google/uuid v1.6.0
|
|
github.com/google/wire v0.5.0
|
|
github.com/grokify/html-strip-tags-go v0.1.0
|
|
github.com/jinzhu/copier v0.4.0
|
|
github.com/jinzhu/now v1.1.5
|
|
github.com/joho/godotenv v1.5.1
|
|
github.com/lib/pq v1.10.9
|
|
github.com/mark3labs/mcp-go v0.43.2
|
|
github.com/microcosm-cc/bluemonday v1.0.27
|
|
github.com/mozillazg/go-pinyin v0.20.0
|
|
github.com/mozillazg/go-unidecode v0.2.0
|
|
github.com/ory/dockertest/v3 v3.11.0
|
|
github.com/robfig/cron/v3 v3.0.1
|
|
github.com/sashabaranov/go-openai v1.41.2
|
|
github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405
|
|
github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f
|
|
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f
|
|
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f
|
|
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f
|
|
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f
|
|
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f
|
|
github.com/spf13/cobra v1.8.1
|
|
github.com/stretchr/testify v1.9.0
|
|
github.com/swaggo/files v1.0.1
|
|
github.com/swaggo/gin-swagger v1.6.0
|
|
github.com/swaggo/swag v1.16.3
|
|
github.com/tidwall/gjson v1.17.3
|
|
github.com/yuin/goldmark v1.7.4
|
|
go.uber.org/mock v0.6.0
|
|
golang.org/x/crypto v0.41.0
|
|
golang.org/x/image v0.20.0
|
|
golang.org/x/net v0.43.0
|
|
golang.org/x/term v0.34.0
|
|
golang.org/x/text v0.28.0
|
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
|
gopkg.in/yaml.v3 v3.0.1
|
|
modernc.org/sqlite v1.33.0
|
|
xorm.io/builder v0.3.13
|
|
xorm.io/xorm v1.3.2
|
|
)
|
|
|
|
require (
|
|
dario.cat/mergo v1.0.1 // indirect
|
|
filippo.io/edwards25519 v1.1.0 // indirect
|
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
|
github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect
|
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
|
github.com/aymerick/douceur v0.2.0 // indirect
|
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
|
github.com/buger/jsonparser v1.1.1 // indirect
|
|
github.com/bytedance/sonic v1.12.2 // indirect
|
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
|
github.com/containerd/continuity v0.4.3 // indirect
|
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
|
github.com/docker/cli v27.2.1+incompatible // indirect
|
|
github.com/docker/docker v27.2.1+incompatible // indirect
|
|
github.com/docker/go-connections v0.5.0 // indirect
|
|
github.com/docker/go-units v0.5.0 // indirect
|
|
github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect
|
|
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
|
|
github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect
|
|
github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 // indirect
|
|
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
|
|
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect
|
|
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
|
|
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
|
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
|
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
|
github.com/go-errors/errors v1.5.1 // indirect
|
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
|
github.com/go-openapi/spec v0.21.0 // indirect
|
|
github.com/go-openapi/swag v0.23.0 // indirect
|
|
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
|
|
github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
|
|
github.com/gogo/protobuf v1.3.2 // indirect
|
|
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
|
|
github.com/golang/snappy v0.0.4 // indirect
|
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
|
github.com/gorilla/css v1.0.1 // indirect
|
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
github.com/invopop/jsonschema v0.13.0 // indirect
|
|
github.com/josharian/intern v1.0.0 // indirect
|
|
github.com/json-iterator/go v1.1.12 // indirect
|
|
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
|
github.com/leodido/go-urn v1.4.0 // indirect
|
|
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
|
|
github.com/lestrrat-go/strftime v1.1.0 // indirect
|
|
github.com/magiconair/properties v1.8.7 // indirect
|
|
github.com/mailru/easyjson v0.7.7 // indirect
|
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
|
github.com/moby/term v0.5.0 // indirect
|
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
|
github.com/opencontainers/runc v1.1.14 // indirect
|
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
|
github.com/pkg/errors v0.9.1 // indirect
|
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
|
github.com/spf13/afero v1.11.0 // indirect
|
|
github.com/spf13/cast v1.7.1 // indirect
|
|
github.com/spf13/pflag v1.0.5 // indirect
|
|
github.com/spf13/viper v1.19.0 // indirect
|
|
github.com/subosito/gotenv v1.6.0 // indirect
|
|
github.com/syndtr/goleveldb v1.0.0 // indirect
|
|
github.com/tidwall/match v1.1.1 // indirect
|
|
github.com/tidwall/pretty v1.2.1 // indirect
|
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
|
go.uber.org/multierr v1.11.0 // indirect
|
|
go.uber.org/zap v1.27.0 // indirect
|
|
golang.org/x/arch v0.10.0 // indirect
|
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
|
golang.org/x/sys v0.35.0 // indirect
|
|
golang.org/x/tools v0.36.0 // indirect
|
|
google.golang.org/protobuf v1.34.2 // indirect
|
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
|
|
modernc.org/mathutil v1.6.0 // indirect
|
|
modernc.org/memory v1.8.0 // indirect
|
|
modernc.org/strutil v1.2.0 // indirect
|
|
modernc.org/token v1.1.0 // indirect
|
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
|
)
|
|
|
|
replace lukechampine.com/uint128 v1.1.1 => github.com/aichy126/uint128 v1.1.1
|
|
|
|
replace modernc.org/cc/v3 v3.40.0 => gitlab.com/cznic/cc/v3 v3.40.0
|
|
|
|
replace github.com/lyft/protoc-gen-validate v0.0.13 => github.com/LinkinStars/protoc-gen-validate v0.0.0-20251030022322-3fddbbe5a0e6
|