Repository source: docs/plans/php_unicode_authored_text_feature_flag_slices_v1.md
PHP Unicode Authored Text Feature Flag Slices V1
This document outlines an atomic implementation plan for supporting readable non-Latin authored text, such as Cyrillic, while preserving inspector-friendly canonical records and rejecting deceptive Unicode.
Goal
Allow human-authored post prose to use visible, normalized Unicode when an explicit feature flag is enabled.
The change should support non-Latin alphabets in fields users read as prose, while keeping machine identifiers ASCII-only and continuing to reject invisible, control, and direction-manipulation characters.
Product Decision
The design frame for this change is:
add a server-side feature flag, tentatively
FORUM_UNICODE_AUTHORED_TEXT=truekeep the default behavior ASCII-only until the flag is enabled
allow Unicode only in authored prose fields:
subjectbody
keep machine-readable fields ASCII-only:
post_idthread_idparent_idboard_tagsprofile slugs
identity IDs
routes and record paths
normalize accepted Unicode prose to NFC before writing canonical records
reject unsafe or non-transparent Unicode even when the flag is enabled
do not transliterate Cyrillic, Greek, or other non-Latin scripts into ASCII
The intended rule is:
Human-authored prose may be Unicode if every character is valid, visible, normalized, and inspectable. Machine identifiers remain ASCII tokens.
Current State
Today:
LocalWriteServiceenforces ASCII for authored subject and body text throughnormalizeAsciiLine()andnormalizeAsciiBody()browser compose normalization rewrites a narrow punctuation set and approved Latin diacritics into ASCII
remaining non-ASCII characters are treated as unsupported and can be explicitly removed
canonical post records are effectively ASCII-only for new writes
route identifiers, tags, profile slugs, and identity IDs are already constrained as ASCII tokens
Relevant current-state references:
docs/plans/php_non_ascii_compose_autocorrection_slices_v1.md (source unavailable)
docs/plans/php_latin_diacritic_transliteration_slices_v1.md (source unavailable)
docs/plans/php_latin_diacritic_transliteration_policy_v1.md (source unavailable)
Unicode Safety Policy
When the feature flag is enabled, authored prose may contain:
valid UTF-8 only
Unicode letters, including non-Latin letters
Unicode combining marks after normalization
Unicode decimal digits
ordinary visible punctuation from an approved set or category policy
plain spaces
newlines in bodies
Authored prose must reject:
invalid UTF-8
control characters other than allowed line breaks
zero-width characters
bidirectional controls, overrides, isolates, and embeddings
private-use characters
surrogates
noncharacters
unassigned code points
format characters
emoji and pictographic symbols unless a later policy explicitly allows them
The policy should be implemented server-side as the authority. Browser validation is an early feedback layer only.
Slice 1: Introduce The Server Feature Flag Boundary
Status:
implemented
added disabled-by-default
FORUM_UNICODE_AUTHORED_TEXTconfig parsingrouted authored subject/body writes through policy-named helpers while preserving current ASCII behavior
documented the flag in the production env example
Focus:
add the rollout switch without changing default write behavior
Checklist:
add one configuration helper for
FORUM_UNICODE_AUTHORED_TEXTdefault the flag to disabled
thread the flag into the write path without changing existing ASCII behavior
keep all existing tests passing with the flag unset
document the flag name and default in the relevant local docs or examples if the repo has an established env-doc location
Expected outcome:
production can ship the flag boundary safely before accepting any Unicode prose
rollback is a config change rather than a code revert
Components touched:
src/ForumRewrite/SiteConfig.phpor the nearest existing config helpersrc/ForumRewrite/Write/LocalWriteService.phpenv docs or examples if appropriate
Slice 2: Add Server-Side Visible Unicode Normalization
Status:
implemented
added
UnicodeTextPolicyfor flag-enabled subject/body normalizationaccepts visible UTF-8 letters such as Cyrillic while rejecting controls, format characters, noncharacters, unsupported spacing, and symbols such as emoji
preserves the existing ASCII validators when
FORUM_UNICODE_AUTHORED_TEXTis disabledadded focused policy tests for Cyrillic acceptance and unsafe-character rejection
Focus:
create the authoritative validation and normalization functions for authored prose
Checklist:
add
normalizeVisibleUnicodeLine()for subject-like one-line proseadd
normalizeVisibleUnicodeBody()for body prosekeep existing
normalizeAsciiLine()andnormalizeAsciiBody()for flag-disabled behaviornormalize accepted Unicode text to NFC before validation and write
reject invalid UTF-8 with a clear error
reject unsafe code point classes listed in the Unicode safety policy
preserve existing line length and body trimming semantics unless a test proves they need adjustment
keep newline handling explicit:
subject lines must not contain line breaks
bodies may contain
\nnormalize
\r\nand\rto\n
Expected outcome:
the server can accept readable Unicode prose only when the flag is enabled
invisible and direction-manipulation characters remain blocked
Components touched:
src/ForumRewrite/Write/LocalWriteService.phppossibly a new small helper under
src/ForumRewrite/Support/if the code would otherwise crowd the write service
Slice 3: Preserve ASCII-Only Machine Fields
Status:
implemented
added flag-enabled smoke coverage proving
board_tagscontinue to canonicalize to ASCIIadded flag-enabled write-service coverage proving Unicode does not widen
thread_id, reaction tags, or OpenPGP identity IDsrelaxed the generic text parser from ASCII-only to valid UTF-8 text so flag-enabled prose can parse while family-specific parsers still enforce machine-field constraints
trimmed board-tag canonicalization after non-ASCII stripping so removed characters cannot leave leading/trailing tag whitespace
Focus:
make sure the Unicode flag cannot accidentally widen identifiers, tags, paths, or routes
Checklist:
confirm
requireAsciiToken()remains unchangedconfirm
normalizeBoardTags()and thread/post tag normalization remain ASCII-onlyconfirm
Author-Identity-IDvalidation remains ASCII-onlyconfirm static artifact route normalization remains ASCII-safe
add explicit tests that the flag does not allow Unicode in:
thread_idparent_idboard_tagsreaction tags
identity IDs
Expected outcome:
Unicode support is limited to human-authored prose
canonical paths and inspector-oriented identifiers remain predictable
Components touched:
src/ForumRewrite/Write/LocalWriteService.phpsrc/ForumRewrite/Canonical/PostRecordParser.phpif parser validation needs clarifying testssrc/ForumRewrite/Host/StaticArtifactBuilder.phponly if route tests reveal drift
Slice 4: Update Browser Compose Normalization For Policy Awareness
Status:
implemented
compose roots now expose the active Unicode-authored-text policy to browser code
browser normalization preserves readable Unicode in
subjectandbodywhen the flag is enabledboard_tagsand other machine fields keep the ASCII normalization pathunsafe Unicode characters remain blocking and explicitly removable
Focus:
align browser feedback with the server policy while keeping the server authoritative
Checklist:
expose or infer whether Unicode authored text is enabled for the rendered compose page
when the flag is disabled:
keep current ASCII normalization behavior
keep non-ASCII characters on the existing unsupported-character path
when the flag is enabled:
allow readable Unicode letters such as Cyrillic in
subjectandbodykeep smart punctuation normalization if it remains a product requirement
stop treating non-Latin letters as removable unsupported characters
surface unsafe or invisible characters as blocking problems
rename browser helper concepts from "unsupported non-ASCII" toward "unsafe characters" where useful
keep explicit removal limited to characters that are actually unsafe under the active policy
ensure the text submitted is the text shown in the fields
Expected outcome:
users get immediate feedback consistent with the feature flag
readable Cyrillic text is no longer presented as something to remove when the flag is enabled
Components touched:
public/assets/browser_signing.jscompose templates that render normalization status or config
focused browser-helper tests
Slice 5: Extend Canonical Parser And Read-Model Coverage
Status:
implemented
added parser coverage for canonical post records containing Unicode subject and body prose
added flag-enabled write smoke coverage for Cyrillic subject/body through canonical record creation, read-model refresh, thread/post rendering, activity/RSS rendering, and static artifact generation
verified generated artifact paths remain ASCII record-id paths while artifact contents preserve Unicode prose
Focus:
verify stored Unicode prose remains parseable and renderable through the existing derived-state pipeline
Checklist:
add parser coverage for canonical post records containing Unicode subject and body text
add read-model rebuild coverage for Unicode subject and body text
add write smoke coverage with the flag enabled for examples such as:
subject:
Приветbody:
Привет мир
verify rendered thread and post pages display the Unicode text correctly
verify static artifact generation handles Unicode text in HTML content without using Unicode in artifact file names
verify RSS or activity views continue to escape and encode output correctly if they include subject/body excerpts
Expected outcome:
Unicode prose survives the full write, parse, read-model, render, and artifact pipeline
file names and routes remain ASCII even when page content is Unicode
Components touched:
tests/CanonicalRecordParsersTest.phptests/WriteApiSmokeTest.phpread-model tests as needed
static artifact tests if existing coverage is convenient
Slice 6: Add Unsafe Unicode Regression Tests
Status:
implemented
added server policy tests for zero-width characters, bidirectional controls, private-use characters, noncharacters, invalid UTF-8, subject line breaks, body controls, and emoji symbols
added browser-helper coverage proving explicit removal drops unsafe Unicode while preserving readable Cyrillic text
Focus:
lock down the anti-deception guarantees
Checklist:
add server tests that reject:
zero-width space
zero-width joiner
right-to-left override
bidirectional isolate/control characters
private-use characters
invalid UTF-8
control characters in subjects
control characters in bodies except normalized newlines
add browser-helper tests that identify the same unsafe characters when the flag is enabled
test that explicit removal removes unsafe characters but preserves readable non-Latin letters
test that the flag-disabled path still blocks or removes non-ASCII according to the current behavior
Expected outcome:
the feature cannot regress into "allow all Unicode"
inspectors are protected against invisible or directionally misleading text
Components touched:
tests/BrowserSigningNormalizationTest.phptests/WriteApiSmokeTest.phpor a narrower write-service testany new Unicode policy helper tests
Slice 7: Rollout Documentation And Operational Checks
Status:
implemented
documented local opt-in in
README.mddocumented production rollout scope, rollback, UTF-8 tooling checks, and the PHP
intlnormalization caveat in the production deploy runbookkept the production env example default commented out and disabled
Focus:
make the feature safe to enable, monitor, and roll back
Checklist:
document the flag:
name
default
intended scope
rollback procedure
document that the flag only affects authored prose, not identifiers or tags
add a short operator note explaining that canonical records may contain UTF-8 prose after the flag is enabled
verify repository tooling, git diffs, rebuild scripts, and static artifact scripts work under a UTF-8 locale
decide whether production examples should keep the flag commented out until rollout
Expected outcome:
operators understand exactly what changes when the flag is enabled
rollback does not require data deletion, but new Unicode-containing records should remain parseable after rollback
Components touched:
README.mdor production runbook if appropriatedocs/examples/env.production.exampleany local env example used for deployment
Guardrails
Do not broaden machine identifiers beyond ASCII.
Do not transliterate non-Latin scripts.
Do not silently remove unsafe characters during ordinary typing.
Do not treat browser validation as authoritative.
Do not allow bidirectional controls or zero-width characters in authored prose.
Do not allow arbitrary Unicode symbols just because they are valid UTF-8.
Do not make the feature default-on until write, rebuild, render, and artifact paths have coverage.
Keep the flag boundary narrow enough that rollback is understandable.
Open Policy Questions
These should be resolved before implementation reaches Slice 2:
Should emoji remain blocked in V1? Recommendation: yes.
Should mathematical symbols and currency symbols be blocked or allowed? Recommendation: block in V1 unless there is a concrete use case.
Should tabs be allowed in bodies? Recommendation: no, normalize or reject them unless existing behavior already permits them.
Should combining marks be accepted only after NFC normalization? Recommendation: yes.
Should mixed scripts be restricted in prose? Recommendation: no for body and subject prose, yes for future display names or identifiers.
Should existing Latin-diacritic ASCII transliteration keep running when Unicode authored text is enabled? Recommendation: keep smart punctuation normalization, but stop transliterating readable letters in subject/body when the user expects Unicode preservation.
Recommended Order
introduce the disabled-by-default server feature flag
add server-side visible Unicode validation and normalization
prove machine fields stay ASCII-only
update browser compose validation for the active policy
cover parser, read-model, render, and artifact paths
add unsafe Unicode regression tests
document rollout and rollback
Acceptance Criteria
This work is complete when:
FORUM_UNICODE_AUTHORED_TEXTdefaults to disabledwith the flag disabled, current ASCII behavior remains unchanged
with the flag enabled,
subjectandbodyaccept readable Cyrillic textunsafe Unicode characters are rejected server-side
browser compose feedback matches the active policy
machine identifiers, tags, routes, profile slugs, and identity IDs remain ASCII-only
Unicode prose survives write, parse, rebuild, render, and static artifact generation
focused tests cover both accepted readable Unicode and rejected deceptive Unicode
Summary
This change is a canonical text policy expansion, not a general Unicode free-for-all.
The desired endpoint is:
readable non-Latin prose works
canonical machine fields remain ASCII and inspectable
invisible and direction-manipulation characters are blocked
rollout is controlled by a feature flag
rollback is operationally straightforward