From 99070a870b25bad790528a87bded15bce97e5282 Mon Sep 17 00:00:00 2001 From: Andreas Ehrencrona Date: Tue, 12 Jan 2021 13:49:39 +0200 Subject: [PATCH] Support for dynamically setting the `lang` attribute --- runtime/src/app/app.ts | 44 ++++++++++++++----- runtime/src/app/types.ts | 9 +++- runtime/src/internal/manifest-client.d.ts | 5 ++- runtime/src/internal/manifest-server.d.ts | 4 +- .../src/server/middleware/get_page_handler.ts | 34 +++++++++----- site/src/template.html | 2 +- .../apps/basics/src/routes/lang/[lang].svelte | 8 ++++ test/apps/basics/src/template.html | 2 +- test/apps/basics/test.ts | 12 +++++ 9 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 test/apps/basics/src/routes/lang/[lang].svelte diff --git a/runtime/src/app/app.ts b/runtime/src/app/app.ts index 1054d99b8..a51a0daa8 100644 --- a/runtime/src/app/app.ts +++ b/runtime/src/app/app.ts @@ -16,6 +16,7 @@ import { HydratedTarget, Target, Redirect, + BranchSegment, Branch, Page, InitialData @@ -136,6 +137,11 @@ async function handle_target(dest: Target): Promise { const { props, branch } = hydrated_target; await render(branch, props, buildPageContext(props, dest.page)); } + + + const { lang } = hydrated_target; + + if (lang) document.querySelector('html').setAttribute('lang', lang); } async function render(branch: Branch, props: any, page: PageContext) { @@ -207,14 +213,21 @@ export async function hydrate_target(dest: Target): Promise { } }; + let lang: string; + if (!root_preloaded) { const root_preload = root_comp.preload || (() => ({})); - root_preloaded = initial_data.preloaded[0] || root_preload.call(preload_context, { + const page_context = { host: page.host, path: page.path, query: page.query, params: {} - }, $session); + }; + root_preloaded = initial_data.preloaded[0] || root_preload.call(preload_context, page_context, $session); + + if (root_comp.lang) { + lang = root_comp.lang(page_context); + } } let branch: Branch; @@ -236,31 +249,38 @@ export async function hydrate_target(dest: Target): Promise { const j = l++; - let result; + let result: BranchSegment; if (!session_dirty && !segment_dirty && current_branch[i] && current_branch[i].part === part.i) { result = current_branch[i]; + lang = result.lang || lang; } else { segment_dirty = false; - const { default: component, preload } = await components[part.i].js(); + const { default: component, preload, lang: get_lang } = await components[part.i].js(); + const page_context = { + host: page.host, + path: page.path, + query: page.query, + params: part.params ? part.params(dest.match) : {} + }; + let preloaded: object; if (ready || !initial_data.preloaded[i + 1]) { preloaded = preload - ? await preload.call(preload_context, { - host: page.host, - path: page.path, - query: page.query, - params: part.params ? part.params(dest.match) : {} - }, $session) + ? await preload.call(preload_context, page_context, $session) : {}; } else { preloaded = initial_data.preloaded[i + 1]; } - result = { component, props: preloaded, segment, match, part: part.i }; + if (get_lang) { + lang = get_lang(page_context); + } + + result = { component, props: preloaded, segment, match, part: part.i, lang }; } return (props[`level${j}`] = result); @@ -271,5 +291,5 @@ export async function hydrate_target(dest: Target): Promise { branch = []; } - return { redirect, props, branch }; + return { redirect, props, branch, lang }; } diff --git a/runtime/src/app/types.ts b/runtime/src/app/types.ts index 1c97ea728..632a9c6d1 100644 --- a/runtime/src/app/types.ts +++ b/runtime/src/app/types.ts @@ -5,14 +5,19 @@ export interface HydratedTarget { preload_error?: any; props: any; branch: Branch; + lang?: string; } -export type Branch = Array<{ +export interface BranchSegment { segment: string; + props?: object; match?: RegExpExecArray; component?: DOMComponentConstructor; part?: number; -}>; + lang?: string; +} + +export type Branch = BranchSegment[]; export type InitialData = { session: any; diff --git a/runtime/src/internal/manifest-client.d.ts b/runtime/src/internal/manifest-client.d.ts index 1b482decf..384888bf1 100644 --- a/runtime/src/internal/manifest-client.d.ts +++ b/runtime/src/internal/manifest-client.d.ts @@ -1,4 +1,4 @@ -import { PageParams } from '@sapper/common'; +import { PageContext, PageParams } from '@sapper/common'; import { Preload } from './shared'; @@ -6,6 +6,7 @@ import { export interface DOMComponentModule { default: DOMComponentConstructor; preload?: Preload; + lang?: (ctx: PageContext) => string; } export interface DOMComponent { @@ -32,5 +33,5 @@ export interface Route { export const ErrorComponent: DOMComponentConstructor; export const components: DOMComponentLoader[]; export const ignore: RegExp[]; -export const root_comp: { preload: Preload }; +export const root_comp: { preload: Preload; lang?: (ctx: PageContext) => string }; export const routes: Route[]; diff --git a/runtime/src/internal/manifest-server.d.ts b/runtime/src/internal/manifest-server.d.ts index fae257b8d..34bb63247 100644 --- a/runtime/src/internal/manifest-server.d.ts +++ b/runtime/src/internal/manifest-server.d.ts @@ -1,5 +1,6 @@ import { - Preload + Preload, + PageContext } from './shared'; export const src_dir: string; @@ -12,6 +13,7 @@ export { SapperRequest, SapperResponse } from '@sapper/server'; export interface SSRComponentModule { default: SSRComponent; preload?: Preload; + lang?: (ctx: PageContext) => string; } export interface SSRComponent { diff --git a/runtime/src/server/middleware/get_page_handler.ts b/runtime/src/server/middleware/get_page_handler.ts index 52ae7ad12..8c99bb491 100644 --- a/runtime/src/server/middleware/get_page_handler.ts +++ b/runtime/src/server/middleware/get_page_handler.ts @@ -172,21 +172,30 @@ export function get_page_handler( let match: RegExpExecArray; let params: Record; + let lang = 'en'; + + const page_context: PageContext = { + host: req.headers.host, + path: req.path, + query: req.query, + params: {} + }; + try { const root_preload = manifest.root_comp.preload || (() => {}); const root_preloaded: PreloadResult = detectClientOnlyReferences(() => root_preload.call( preload_context, - { - host: req.headers.host, - path: req.path, - query: req.query, - params: {} - }, + page_context, session ) ); + if (manifest.root_comp.lang) { + lang = detectClientOnlyReferences(() => + manifest.root_comp.lang(page_context)); + } + match = error ? null : page.pattern.exec(req.path); let toPreload: PreloadResult[] = [root_preloaded]; @@ -197,16 +206,16 @@ export function get_page_handler( // the deepest level is used below, to initialise the store params = part.params ? part.params(match) : {}; + if (part.component.lang) { + lang = detectClientOnlyReferences(() => + part.component.lang({ ...page_context, params })); + } + return part.component.preload ? detectClientOnlyReferences(() => part.component.preload.call( preload_context, - { - host: req.headers.host, - path: req.path, - query: req.query, - params - }, + { ...page_context, params }, session ) ) @@ -381,6 +390,7 @@ export function get_page_handler( .replace('%sapper.html%', () => html) .replace('%sapper.head%', () => head) .replace('%sapper.styles%', () => styles) + .replace('%sapper.lang%', () => lang) .replace(/%sapper\.cspnonce%/g, () => nonce_value); res.statusCode = status; diff --git a/site/src/template.html b/site/src/template.html index f75784e99..d28cdef3a 100644 --- a/site/src/template.html +++ b/site/src/template.html @@ -1,5 +1,5 @@ - + diff --git a/test/apps/basics/src/routes/lang/[lang].svelte b/test/apps/basics/src/routes/lang/[lang].svelte new file mode 100644 index 000000000..21de05873 --- /dev/null +++ b/test/apps/basics/src/routes/lang/[lang].svelte @@ -0,0 +1,8 @@ + + +sv +en diff --git a/test/apps/basics/src/template.html b/test/apps/basics/src/template.html index 75027cea6..d0c8b235b 100644 --- a/test/apps/basics/src/template.html +++ b/test/apps/basics/src/template.html @@ -1,5 +1,5 @@ - + diff --git a/test/apps/basics/test.ts b/test/apps/basics/test.ts index b0ded4375..bce8124e5 100644 --- a/test/apps/basics/test.ts +++ b/test/apps/basics/test.ts @@ -277,6 +277,18 @@ describe('basics', function() { assert.equal(html.indexOf('%sapper'), -1); }); + it('replaces %sapper.lang%', async () => { + await r.load('/lang/sv'); + await r.sapper.start(); + + const get_document_lang = () => r.page.evaluate(() => document.documentElement.lang); + assert.equal(await get_document_lang(), 'sv'); + + await r.page.click('#en'); + await r.wait(); + assert.equal(await get_document_lang(), 'en'); + }); + it('navigates between routes with empty parts', async () => { await r.load('/dirs/foo'); await r.sapper.start();