Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions website/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const config: Config = {

organizationName: 'aliyun',
projectName: 'iac-code',
clientModules: ['./src/clientModules/localeRedirect.js'],

onBrokenLinks: 'throw',
markdown: {
Expand Down
1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"test": "node --test src/clientModules/*.test.cjs",
"build": "docusaurus build",
"serve": "docusaurus serve",
"clear": "docusaurus clear",
Expand Down
51 changes: 51 additions & 0 deletions website/src/clientModules/localeRedirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import siteConfig from '@generated/docusaurus.config';
import redirectRules from './localeRedirectRules.cjs';

const {getBrowserLocaleRedirect} = redirectRules;
const storageKey = 'iac-code.localeRedirect.v1';

function wasRedirectedInSession() {
try {
return window.sessionStorage.getItem(storageKey) === 'true';
} catch {
return false;
}
}

function markRedirectedInSession() {
try {
window.sessionStorage.setItem(storageKey, 'true');
} catch {
// Storage can be unavailable in strict browser privacy modes.
}
}

function getBrowserLanguages() {
if (Array.isArray(window.navigator.languages) && window.navigator.languages.length > 0) {
return window.navigator.languages;
}

return window.navigator.language ? [window.navigator.language] : [];
}

export function onRouteDidUpdate({location}) {
if (typeof window === 'undefined') {
return;
}

const redirectTo = getBrowserLocaleRedirect({
baseUrl: siteConfig.baseUrl,
defaultLocale: siteConfig.i18n.defaultLocale,
locales: siteConfig.i18n.locales,
pathname: location.pathname,
search: location.search,
hash: location.hash,
browserLanguages: getBrowserLanguages(),
alreadyRedirected: wasRedirectedInSession(),
});

if (redirectTo) {
markRedirectedInSession();
window.location.replace(redirectTo);
}
}
85 changes: 85 additions & 0 deletions website/src/clientModules/localeRedirect.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const assert = require('node:assert/strict');
const test = require('node:test');

const {
getBrowserLocaleRedirect,
selectPreferredLocale,
} = require('./localeRedirectRules.cjs');

const config = {
baseUrl: '/iac-code/',
defaultLocale: 'en',
locales: ['en', 'zh-Hans', 'ja', 'fr', 'de', 'es', 'pt'],
};

test('selectPreferredLocale maps browser language variants to configured locales', () => {
assert.equal(selectPreferredLocale(['zh-CN', 'en-US'], config), 'zh-Hans');
assert.equal(selectPreferredLocale(['ja-JP', 'en-US'], config), 'ja');
assert.equal(selectPreferredLocale(['pt-BR', 'en-US'], config), 'pt');
assert.equal(selectPreferredLocale(['it-IT', 'fr-FR'], config), 'fr');
});

test('selectPreferredLocale falls back to the default locale for unsupported languages', () => {
assert.equal(selectPreferredLocale(['it-IT'], config), 'en');
});

test('getBrowserLocaleRedirect redirects only the root page to a non-default browser locale', () => {
assert.equal(
getBrowserLocaleRedirect({
...config,
pathname: '/iac-code/',
search: '?utm_source=test',
hash: '#top',
browserLanguages: ['zh-CN', 'en-US'],
alreadyRedirected: false,
}),
'/iac-code/zh-Hans/?utm_source=test#top',
);
});

test('getBrowserLocaleRedirect skips localized, non-root, default-locale, and already-redirected pages', () => {
assert.equal(
getBrowserLocaleRedirect({
...config,
pathname: '/iac-code/zh-Hans/docs/intro',
search: '',
hash: '',
browserLanguages: ['zh-CN'],
alreadyRedirected: false,
}),
null,
);
assert.equal(
getBrowserLocaleRedirect({
...config,
pathname: '/iac-code/docs/intro',
search: '',
hash: '',
browserLanguages: ['zh-CN'],
alreadyRedirected: false,
}),
null,
);
assert.equal(
getBrowserLocaleRedirect({
...config,
pathname: '/iac-code/',
search: '',
hash: '',
browserLanguages: ['en-US'],
alreadyRedirected: false,
}),
null,
);
assert.equal(
getBrowserLocaleRedirect({
...config,
pathname: '/iac-code/',
search: '',
hash: '',
browserLanguages: ['zh-CN'],
alreadyRedirected: true,
}),
null,
);
});
69 changes: 69 additions & 0 deletions website/src/clientModules/localeRedirectRules.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
function normalizeBaseUrl(baseUrl) {
const withLeadingSlash = baseUrl.startsWith('/') ? baseUrl : `/${baseUrl}`;
return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`;
}

function normalizePathname(pathname) {
const withLeadingSlash = pathname.startsWith('/') ? pathname : `/${pathname}`;
return withLeadingSlash.endsWith('/index.html')
? withLeadingSlash.slice(0, -'index.html'.length)
: withLeadingSlash;
}

function resolveConfiguredLocale(browserLanguage, locales) {
const normalized = browserLanguage.toLowerCase().replace('_', '-');
const localeByLowercase = new Map(locales.map((locale) => [locale.toLowerCase(), locale]));

if (normalized.startsWith('zh') && localeByLowercase.has('zh-hans')) {
return localeByLowercase.get('zh-hans');
}

const exactLocale = localeByLowercase.get(normalized);
if (exactLocale) {
return exactLocale;
}

const language = normalized.split('-')[0];
return localeByLowercase.get(language) ?? null;
}

function selectPreferredLocale(browserLanguages, config) {
for (const browserLanguage of browserLanguages) {
const locale = resolveConfiguredLocale(browserLanguage, config.locales);
if (locale) {
return locale;
}
}

return config.defaultLocale;
}

function isRootPath(pathname, baseUrl) {
const normalizedPathname = normalizePathname(pathname);
return normalizedPathname === normalizeBaseUrl(baseUrl);
}

function getBrowserLocaleRedirect({
pathname,
search = '',
hash = '',
browserLanguages,
alreadyRedirected,
...config
}) {
if (alreadyRedirected || !isRootPath(pathname, config.baseUrl)) {
return null;
}

const preferredLocale = selectPreferredLocale(browserLanguages, config);
if (preferredLocale === config.defaultLocale) {
return null;
}

return `${normalizeBaseUrl(config.baseUrl)}${preferredLocale}/${search}${hash}`;
}

module.exports = {
getBrowserLocaleRedirect,
selectPreferredLocale,
};
Loading