85 Commits

Author SHA1 Message Date
LinkinStars f92a0fe140 Harden Accept-Language parsing 2026-07-10 10:19:39 +08:00
Ahmed Qasid a6d867e4cc fix: avoid topic fallback for non-Latin titles via pragmatic ASCII transliteration (#1526)
# 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>
2026-07-10 10:19:39 +08:00
LinkinStars ce5aadf30d feat: add AI configuration support with related controllers and services 2026-01-23 17:25:42 +08:00
maishivamhoo123 1fbb802e8f feat: load optional .env file and add .env.example 2026-01-14 11:26:00 +08:00
ferhat elmas bc629db132 chore(deps): bump mockgen to 0.6.0 for go1.25 support
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
2025-11-28 17:00:25 +08:00
LinkinStars 20cd4d7c44 fix(deps): update protoc-gen-validate dependency to a new version
Build Latest Docker Image For Release / build (push) Has been cancelled
Build Binary For Release / build-goreleaser (push) Has been cancelled
Build Docker Image For Release / build (push) Has been cancelled
2025-10-30 10:47:10 +08:00
Sonui 93e183be62 feat: Add resetPassword cli tool 2025-10-28 14:52:53 +08:00
LinkinStars a62c975c7e chore(deps): update Go version and dependencies to v1.23.0 and latest compatible versions 2025-06-24 10:42:32 +08:00
LinkinStars 1fe3b7d8fc refactor(gomock): update gomock dependence and docs 2024-12-09 11:52:18 +08:00
LinkinStars d6fa819fa1 refactor(gomod): downgrade dependency 2024-09-27 14:02:38 +08:00
LinkinStars e062cbcb0e refactor(gomod): replace pinyin dependency 2024-09-26 16:25:00 +08:00
LinkinStars 0d6ea89ad3 refactor(gomod): upgrade golang and dependence version 2024-09-23 15:00:34 +08:00
LinkinStars cb5a0b7045 chore(lint): lint code and regenerate docs 2024-09-04 10:26:57 +08:00
ferrischi201 eacb79e512 feat(recommend): add tag-based recommendations in question page
Filter out deleted or hidden questions and support multi databases and filter out deleted or hidden questions.

For issue:1003.
2024-09-04 10:24:53 +08:00
Jan Ernsting 15531f4223 build swagger documentation on go generate 2024-08-22 10:58:06 +08:00
LinkinStars bbb516c213 chore(gomod): remove go mod 2024-04-09 12:22:08 +08:00
LinkinStars abe0680641 Release/1.3.0 (#876)
Co-authored-by: shuai <lishuailing@sifou.com>
Co-authored-by: Han Gao <dhangao@hotmail.com>
Co-authored-by: wzy <770954908@qq.com>
Co-authored-by: zahash <zahash.z@gmail.com>
Co-authored-by: Hosein Beigi <hbsciw@gmail.com>
Co-authored-by: kumfo <kumfo@sifou.com>
Co-authored-by: sy-records <52o@qq52o.cn>
Co-authored-by: 而立 <cuihengqing@cai-inc.com>
Co-authored-by: foxzero-007 <chq2549603631@gmail.com>
Co-authored-by: easy <58644520+foxzero-007@users.noreply.github.com>
Co-authored-by: robin <robin@rnode.me>
Co-authored-by: Akhil <surapuramakhil@gmail.com>
Co-authored-by: Shaobiao Lin <whalevocal@gmail.com>
2024-03-22 17:25:46 +08:00
LinkinStars 84ef7d0e55 ci(asf): change the check-asf-header script to docker command via hawkeye-native 2023-11-17 15:54:27 +08:00
LinkinStars a72ebc5262 Add dependencies license for release binary (#610)
The files that in the `docs/release` will be pack into binary release
zip.

- [x] put all the dependency licenses into `docs/release/licenses`.
- [x] add LICENSE for release binary. (We need to check this format
whether meet the requirements or not.)
- [x] add NOTICE for release binary.

---------

Co-authored-by: tison <wander4096@gmail.com>
Co-authored-by: shuai <lishuailing@sifou.com>
2023-11-15 15:17:11 +08:00
LinkinStars d1733b24fe chore(gomod): upgrade dockertest go mod version 2023-09-06 14:31:11 +08:00
realize096 631e24ca22 update github.com/opencontainers/runc v1.1.2 to 1.1.5 2023-09-06 14:26:33 +08:00
LinkinStars 119c7ddf6d Merge branch 'feat/1.1.2/notification' into test 2023-08-22 17:40:49 +08:00
LinkinStars e4a77367a4 feat(notification): add notification limit 2023-08-22 17:07:45 +08:00
LinkinStars 8a037f00db feat(cache): upgrade cache interface 2023-08-22 16:10:41 +08:00
LinkinStars 5d28954e4c feat(question): change create question title function 2023-08-18 18:15:44 +08:00
aichy c7774e8dd8 Merge branch 'feat/1.1.1/ai' into test 2023-08-02 11:48:51 +08:00
aichy 1034f05644 go mod 2023-08-02 11:48:26 +08:00
aichy 7e56b758e3 Merge branch 'feat/1.1.1/ai' into test 2023-07-28 17:12:36 +08:00
aichy 4fd7401566 update go mod 2023-07-28 17:11:35 +08:00
aichy 2f25686af3 Merge branch 'feat/1.1.1/ai' into test 2023-07-28 16:53:57 +08:00
aichy d59d5ca0a8 fix go mod 2023-07-28 16:53:35 +08:00
aichy c96b55a987 update go.mod 2023-07-24 11:20:40 +08:00
aichy 7bb7f84432 Merge branch 'feat/1.1.1/ai' into test 2023-07-24 10:45:24 +08:00
aichy f5f98bd602 swagger 2023-07-19 16:59:23 +08:00
LinkinStars 3988501fc5 feat(rank): adjust the rank of voting question and answer. 2023-07-19 16:48:26 +08:00
LinkinStars 622c517da7 refactor(data): use names for gonic mapper 2023-06-21 16:41:14 +08:00
LinkinStars 40690a2a36 fix(notification): add rank activity type translation 2023-05-29 10:38:59 +08:00
LinkinStars ecb0b3c06b chore(gomod): update go mod 2023-05-16 19:09:05 +08:00
LinkinStars 350df2da8b feat(email): translator all email template 2023-05-16 17:47:26 +08:00
LinkinStars fb3b6a862f chore(gomod): update go mod and swagger doc 2023-05-09 10:54:42 +08:00
LinkinStars 0f7121d6fe fix(merge): update go mod 2023-04-14 18:24:09 +08:00
LinkinStars 2a121c573b feat(router): add middleware for user router 2023-04-03 11:17:38 +08:00
LinkinStars 9bd9b475f4 Merge branch 'feat/ui-1.1.0' into feature-plugin
# Conflicts:
#	internal/base/reason/reason.go
#	internal/schema/user_schema.go
#	internal/service/user_service.go
2023-03-21 11:45:50 +08:00
aichy126 ecff4e02bc update short id 2023-03-20 11:00:34 +08:00
LinkinStars adcb47a5e1 fix(plugin): Add cache plugin interface 2023-03-08 17:14:55 +08:00
LinkinStars 4a8ae2b166 Merge branch 'dev' into feature-plugin 2023-03-07 15:39:07 +08:00
aichy126 f357f6219b update swagger 2023-02-24 17:37:01 +08:00
LinkinStars 6c70bc814e refactor(gomod): update go mod 2023-02-21 17:41:26 +08:00
LinkinStars 6cd7419627 fix(gomod): remove the require that is not needed 2023-02-21 14:34:22 +08:00
LinkinStars 025d0d9821 Merge branch 'github-dev' into release/1.0.5
# Conflicts:
#	go.mod
2023-02-21 10:17:35 +08:00