Skip to content

Commit 589ac1b

Browse files
committed
feat(i18n): added fallback non-wasm locale redirection
Perseus will now work with i18n if JS, Wasm, or both are disabled, meaning progressive enhancement is now complete.
1 parent de1c217 commit 589ac1b

File tree

3 files changed

+62
-7
lines changed

3 files changed

+62
-7
lines changed

packages/perseus-actix-web/src/initial_load.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use perseus::{
88
error_pages::ErrorPageData,
99
i18n::{TranslationsManager, Translator},
1010
router::{match_route, RouteInfo, RouteVerdict},
11-
serve::{get_page_for_template, interpolate_page_data},
11+
serve::{
12+
get_page_for_template, interpolate_locale_redirection_fallback, interpolate_page_data,
13+
},
1214
},
1315
stores::{ImmutableStore, MutableStore},
1416
ErrorPages, SsrNode,
@@ -150,12 +152,16 @@ pub async fn initial_load<M: MutableStore, T: TranslationsManager>(
150152
http_res.body(final_html)
151153
}
152154
// For locale detection, we don't know the user's locale, so there's not much we can do except send down the app shell, which will do the rest and fetch from `.perseus/page/...`
153-
RouteVerdict::LocaleDetection(_) => {
155+
RouteVerdict::LocaleDetection(path) => {
154156
// We use a `302 Found` status code to indicate a redirect
155157
// We 'should' generate a `Location` field for the redirect, but it's not RFC-mandated, so we can use the app shell
156-
HttpResponse::Found()
157-
.content_type("text/html")
158-
.body(html_shell.get_ref())
158+
HttpResponse::Found().content_type("text/html").body(
159+
interpolate_locale_redirection_fallback(
160+
html_shell.get_ref(),
161+
// We'll redirect the user to the default locale
162+
&format!("{}/{}", opts.locales.default, path),
163+
),
164+
)
159165
}
160166
RouteVerdict::NotFound => html_err(404, "page not found"),
161167
}

packages/perseus/src/export.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::errors::*;
2-
use crate::html_shell::{interpolate_page_data, prep_html_shell};
2+
use crate::html_shell::{
3+
interpolate_locale_redirection_fallback, interpolate_page_data, prep_html_shell,
4+
};
35
use crate::locales::Locales;
46
use crate::serve::get_render_cfg;
57
use crate::serve::PageData;
@@ -87,12 +89,16 @@ pub async fn export_app(
8789
};
8890
// Create a locale detection file for it if we're using i18n
8991
// These just send the app shell, which will perform a redirect as necessary
92+
// Notably, these also include fallback redirectors if either Wasm or JS is disabled (or both)
9093
// TODO put everything inside its own folder for initial loads?
9194
if locales.using_i18n {
9295
immutable_store
9396
.write(
9497
&format!("exported/{}.html", &initial_load_path),
95-
&html_shell,
98+
&interpolate_locale_redirection_fallback(
99+
&html_shell,
100+
&format!("{}/{}", locales.default, &path),
101+
),
96102
)
97103
.await?;
98104
}

packages/perseus/src/html_shell.rs

+43
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,46 @@ pub fn interpolate_page_data(html_shell: &str, page_data: &PageData, root_id: &s
9999
.replace(&html_to_replace_double, &html_replacement)
100100
.replace(&html_to_replace_single, &html_replacement)
101101
}
102+
103+
/// Intepolates a fallback for locale redirection pages such that, even if JavaScript is disabled, the user will still be redirected to the default locale.
104+
/// From there, Perseus' inbuilt progressive enhancement can occur, but without this a user directed to an unlocalized page with JS disabled would see a
105+
/// blank screen, which is terrible UX. Note that this also includes a fallback for if JS is enable dbut Wasm is disabled.
106+
pub fn interpolate_locale_redirection_fallback(html_shell: &str, redirect_url: &str) -> String {
107+
// This will be used if JavaScript is completely disabled (it's then the site's responsibility to show a further message)
108+
let dumb_redirector = format!(
109+
r#"<noscript>
110+
<meta http-equiv="refresh" content="0; url=/{}" />
111+
</noscript>"#,
112+
redirect_url
113+
);
114+
// This will be used if JS is enabled, but Wasm is disabled or not supported (it's then the site's responsibility to show a further message)
115+
// Wasm support detector courtesy https://stackoverflow.com/a/47880734
116+
let js_redirector = format!(
117+
r#"<script>
118+
function wasmSupported() {{
119+
try {{
120+
if (typeof WebAssembly === "object"
121+
&& typeof WebAssembly.instantiate === "function") {{
122+
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
123+
if (module instanceof WebAssembly.Module) {{
124+
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
125+
}}
126+
}}
127+
}} catch (e) {{}}
128+
return false;
129+
}}
130+
131+
if (!wasmSupported()) {{
132+
window.location.replace("{}");
133+
}}
134+
</script>"#,
135+
redirect_url
136+
);
137+
138+
let html = html_shell.replace(
139+
"</head>",
140+
&format!("{}\n{}\n</head>", js_redirector, dumb_redirector),
141+
);
142+
143+
html
144+
}

0 commit comments

Comments
 (0)