ReviewOS

also looking at this

stacks/stx

fix: translate fragment navigation titles

#2120
Merged chris wants to merge codex/farming-navigation-metadata into main
2 files +76 -0

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/bun-plugin/src/serve.tsmodified+9-0
Changes to packages/bun-plugin/src/serve.ts
@@ -3748,6 +3748,15 @@ function __stxOverlay(errs){
37483748 html = ensureRouterConfig(html)
37493749
37503750 const headers = new Headers(res.headers)
3751 const fragmentTitle = headers.get('X-STX-Title')
3752 if (res.headers.get('X-STX-Fragment') === 'true' && fragmentTitle && i18nConfig && stx.applyTranslations) {
3753 // The fragment's title was captured before this final translation pass.
3754 // Treat it as HTML text while translating, then return plain title text
3755 // to the router. Escaping first preserves literal entities in the title.
3756 const titleHtml = escapeHtml(decodeURIComponent(fragmentTitle))
3757 const translated = stx.applyTranslations(titleHtml, i18nConfig, locale)
3758 headers.set('X-STX-Title', encodeURIComponent(decodeTitleEntities(translated)))
3759 }
37513760 headers.delete('content-length')
37523761 return new Response(html, {
37533762 status: res.status,
packages/bun-plugin/test/serve-fragment-i18n-title.test.tsadded+67-0
Changes to packages/bun-plugin/test/serve-fragment-i18n-title.test.ts
@@ -0,0 +1,67 @@
1import { afterAll, beforeAll, expect, test } from 'bun:test'
2import { rm } from 'node:fs/promises'
3import path from 'node:path'
4import { createTempDir } from '../../stx/src/temp-dir'
5import { freePort } from '../../stx/test-utils/test-port'
6
7const port = freePort()
8const base = `http://127.0.0.1:${port}`
9const serveSource = path.join(import.meta.dir, '../src/serve.ts')
10let directory: string
11let child: ReturnType<typeof Bun.spawn> | undefined
12let output: Promise<string> | undefined
13let errors: Promise<string> | undefined
14
15beforeAll(async () => {
16 directory = createTempDir('stx-fragment-title-i18n-')
17 await Bun.write(path.join(directory, 'views/page.stx'), `<!DOCTYPE html>
18<html><head><title>{t:page.title} | 100% &amp; &amp;lt;</title></head>
19<body><main><h1>{t:page.title}</h1></main></body></html>`)
20 await Bun.write(path.join(directory, 'views/literal.stx'), `<!DOCTYPE html>
21<html><head><title>Literal &amp;lt; 100%</title></head><body><main>Literal</main></body></html>`)
22 await Bun.write(path.join(directory, 'driver.ts'), `import { serve } from ${JSON.stringify(serveSource)}
23await serve({ patterns: ['views'], port: ${port}, imageWarmup: false,
24 i18n: { locales: ['en', 'de', 'nl'], defaultLocale: 'en', labels: {}, pickerSelector: '#lang-picker',
25 translations: { en: { 'page.title': 'English & "quoted" <literal> 你好' }, de: { 'page.title': 'Deutsch & "zitiert" <literal> Grüß' } } }
26})`)
27 child = Bun.spawn([process.execPath, 'driver.ts'], { cwd: directory, stdout: 'pipe', stderr: 'pipe' })
28 output = new Response(child.stdout).text()
29 errors = new Response(child.stderr).text()
30 const deadline = Date.now() + 30_000
31 while (true) {
32 try { await fetch(`${base}/not-a-page`); break }
33 catch {
34 if (Date.now() > deadline) throw new Error('Title regression server did not start')
35 await Bun.sleep(50)
36 }
37 }
38}, 35_000)
39
40afterAll(async () => {
41 child?.kill()
42 if (child) await child.exited
43 await output
44 await errors
45 if (directory) await rm(directory, { recursive: true, force: true })
46})
47
48for (const [route, expected] of [
49 ['/page', 'English & "quoted" <literal> 你好 | 100% & &lt;'],
50 ['/de/page', 'Deutsch & "zitiert" <literal> Grüß | 100% & &lt;'],
51 ['/nl/page', 'English & "quoted" <literal> 你好 | 100% & &lt;'],
52 ['/literal', 'Literal &lt; 100%'],
53]) {
54 test(`fragment title uses the destination locale and preserves text: ${route}`, async () => {
55 const full = await fetch(`${base}${route}`)
56 expect(full.status).toBe(200)
57 const html = await full.text()
58 expect(html).not.toContain('{t:page.title}')
59 const fragment = await fetch(`${base}${route}`, { headers: { 'X-STX-Router': 'true' } })
60 expect(fragment.status).toBe(200)
61 expect(fragment.headers.get('X-STX-Fragment')).toBe('true')
62 const title = fragment.headers.get('X-STX-Title')
63 expect(title).not.toBeNull()
64 expect(decodeURIComponent(title!)).toBe(expected)
65 expect(await fragment.text()).not.toContain('{t:page.title}')
66 })
67}