-
Notifications
You must be signed in to change notification settings - Fork 923
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Change the locale dynamically by adding &i18n-locale to URL
The main issue was the inability to dynamically change the locale in OpenSearch Dashboards. Currently we need to update config file and i18nrc.json. This PR allows users to switch to a different locale (e.g., from English to Chinese) by appending or modifying the 'i18n-locale' parameter in the URL. * getAndUpdateLocaleInUrl: If a non-default locale is found, this function reconstructs the URL with the locale parameter in the correct position. * updated the ScopedHistory class, allowing it to detect locale changes and trigger reloads as necessary. * modify the i18nMixin, which sets up the i18n system during server startup, to register all available translation files during server startup, not just the current locale. * update the uiRenderMixin to accept requests for any registered locale and dynamically load and cache translations for requested locales. Signed-off-by: Anan Zhuang <[email protected]>
- Loading branch information
Showing
7 changed files
with
483 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { extractLocaleInfo, getAndUpdateLocaleInUrl } from './locale_helper'; | ||
|
||
describe('extractLocaleInfo', () => { | ||
const testCases = [ | ||
{ | ||
description: 'After hash and slash', | ||
input: 'http://localhost:5603/app/home#/&i18n-locale=fr-FR', | ||
expected: { | ||
localeValue: 'fr-FR', | ||
localeParam: 'i18n-locale=fr-FR', | ||
updatedUrl: 'http://localhost:5603/app/home#/', | ||
}, | ||
}, | ||
{ | ||
description: 'After path and slash', | ||
input: 'http://localhost:5603/app/home/&i18n-locale=de-DE', | ||
expected: { | ||
localeValue: 'de-DE', | ||
localeParam: 'i18n-locale=de-DE', | ||
updatedUrl: 'http://localhost:5603/app/home/', | ||
}, | ||
}, | ||
{ | ||
description: 'No locale parameter', | ||
input: 'http://localhost:5603/app/home', | ||
expected: { | ||
localeValue: 'en', | ||
localeParam: null, | ||
updatedUrl: 'http://localhost:5603/app/home', | ||
}, | ||
}, | ||
{ | ||
description: 'Complex URL with locale', | ||
input: 'http://localhost:5603/app/dashboards#/view/id?_g=(...)&_a=(...)&i18n-locale=es-ES', | ||
expected: { | ||
localeValue: 'es-ES', | ||
localeParam: 'i18n-locale=es-ES', | ||
updatedUrl: 'http://localhost:5603/app/dashboards#/view/id?_g=(...)&_a=(...)', | ||
}, | ||
}, | ||
]; | ||
|
||
testCases.forEach(({ description, input, expected }) => { | ||
it(description, () => { | ||
const result = extractLocaleInfo(input); | ||
expect(result).toEqual(expected); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('getAndUpdateLocaleInUrl', () => { | ||
let originalHistoryReplaceState: typeof window.history.replaceState; | ||
|
||
beforeEach(() => { | ||
// Mock window.history.replaceState | ||
originalHistoryReplaceState = window.history.replaceState; | ||
window.history.replaceState = jest.fn(); | ||
}); | ||
|
||
afterEach(() => { | ||
// Restore original window.history.replaceState | ||
window.history.replaceState = originalHistoryReplaceState; | ||
}); | ||
|
||
const testCases = [ | ||
{ | ||
description: 'Category 1: basePath + #/', | ||
input: 'http://localhost:5603/app/home#/&i18n-locale=zh-CN', | ||
expected: 'http://localhost:5603/app/home#/?i18n-locale=zh-CN', | ||
locale: 'zh-CN', | ||
}, | ||
{ | ||
description: 'Category 1: basePath + # (empty hashPath)', | ||
input: 'http://localhost:5603/app/home#&i18n-locale=zh-CN', | ||
expected: 'http://localhost:5603/app/home#?i18n-locale=zh-CN', | ||
locale: 'zh-CN', | ||
}, | ||
{ | ||
description: 'Category 2: basePath + # + hashPath + ? + hashQuery', | ||
input: 'http://localhost:5603/app/dashboards#/view/id?_g=(...)&_a=(...)&i18n-locale=zh-CN', | ||
expected: 'http://localhost:5603/app/dashboards#/view/id?_g=(...)&_a=(...)&i18n-locale=zh-CN', | ||
locale: 'zh-CN', | ||
}, | ||
{ | ||
description: 'Category 3: basePath only', | ||
input: 'http://localhost:5603/app/management&i18n-locale=zh-CN', | ||
expected: 'http://localhost:5603/app/management?i18n-locale=zh-CN', | ||
locale: 'zh-CN', | ||
}, | ||
{ | ||
description: 'Category 1: basePath + # + hashPath', | ||
input: 'http://localhost:5603/app/dev_tools#/console&i18n-locale=zh-CN', | ||
expected: 'http://localhost:5603/app/dev_tools#/console?i18n-locale=zh-CN', | ||
locale: 'zh-CN', | ||
}, | ||
{ | ||
description: 'URL without locale parameter', | ||
input: 'http://localhost:5603/app/home#/', | ||
expected: 'http://localhost:5603/app/home#/', | ||
locale: 'en', | ||
}, | ||
{ | ||
description: 'Complex URL with multiple parameters', | ||
input: | ||
"http://localhost:5603/app/dashboards#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=(filters:!(),refreshInterval:(pause:!f,value:900000),time:(from:now-24h,to:now))&_a=(description:'Analyze%20mock%20flight%20data',filters:!())&i18n-locale=zh-CN", | ||
expected: | ||
"http://localhost:5603/app/dashboards#/view/7adfa750-4c81-11e8-b3d7-01146121b73d?_g=(filters:!(),refreshInterval:(pause:!f,value:900000),time:(from:now-24h,to:now))&_a=(description:'Analyze%20mock%20flight%20data',filters:!())&i18n-locale=zh-CN", | ||
locale: 'zh-CN', | ||
}, | ||
]; | ||
|
||
testCases.forEach(({ description, input, expected, locale }) => { | ||
it(description, () => { | ||
const result = getAndUpdateLocaleInUrl(input); | ||
expect(result).toBe(locale); | ||
if (locale !== 'en') { | ||
expect(window.history.replaceState).toHaveBeenCalledWith(null, '', expected); | ||
} else { | ||
expect(window.history.replaceState).not.toHaveBeenCalled(); | ||
} | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
/** | ||
* Extracts the locale value and parameter from a given URL string. | ||
* | ||
* @param url - The full URL string to parse | ||
* @returns An object with localeValue, localeParam or null if not found, and updatedUrl or url if no updates. | ||
*/ | ||
export function extractLocaleInfo( | ||
url: string | ||
): { localeValue: string | null; localeParam: string | null; updatedUrl: string } { | ||
const patterns = [ | ||
/[#&?](i18n-locale)=([^&/]+)/i, // Standard query parameter | ||
/#\/&(i18n-locale)=([^&/]+)/i, // After hash and slash | ||
/\/&(i18n-locale)=([^&/]+)/i, // After path and slash | ||
]; | ||
|
||
for (const pattern of patterns) { | ||
const match = url.match(pattern); | ||
if (match) { | ||
const localeValue = match[2]; | ||
const localeParam = `${match[1]}=${match[2]}`; | ||
const updatedUrl = url.replace(match[0], ''); | ||
return { localeValue, localeParam, updatedUrl }; | ||
} | ||
} | ||
|
||
return { localeValue: 'en', localeParam: null, updatedUrl: url }; | ||
} | ||
|
||
/** | ||
* Extracts a dynamically added locale parameter from a URL and restructures the URL | ||
* to include this locale parameter in a consistent, functional manner. | ||
* | ||
* This function is specifically designed to handle cases where '&i18n-locale=<locale>' | ||
* has been appended to the URL, potentially in a position that could cause issues | ||
* with OpenSearch Dashboards' URL parsing or functionality. | ||
* | ||
* The restructuring is necessary because simply appending the locale parameter | ||
* to certain URL structures can lead to parsing errors or the parameter being ignored. | ||
* | ||
* The function handles various URL structures to ensure the locale parameter | ||
* is placed in a position where it will be correctly parsed and utilized by | ||
* OpenSearch Dashboards, while maintaining the integrity of existing URL components. | ||
* | ||
* URL Components: | ||
* - basePath: The part of the URL before the hash (#). It typically includes the domain and application path. | ||
* - hashPath: The part of the URL after the hash (#) but before any query parameters (?). | ||
* - hashQuery: The query parameters after the hashPath. In OpenSearch Dashboards, this often contains | ||
* RISON-encoded data and never ends with a slash (/) to avoid RISON parsing errors. | ||
* | ||
* This function handles three main categories of URLs: | ||
* 1. basePath + # + hashPath (including when hashPath is '/' or empty) | ||
* Before: basePath#hashPath&i18n-locale=zh-CN | ||
* After: basePath#hashPath?i18n-locale=zh-CN | ||
* Restructuring rationale: The '&' is changed to '?' because there were no existing | ||
* query parameters after the hashPath. This ensures the locale is treated as a proper | ||
* query parameter and not mistakenly considered part of the hashPath. | ||
* | ||
* 2. basePath + # + hashPath + ? + hashQuery | ||
* Before: basePath#hashPath?hashQuery&i18n-locale=zh-CN | ||
* After: basePath#hashPath?hashQuery&i18n-locale=zh-CN | ||
* Restructuring rationale: The locale parameter is appended to existing query parameters. | ||
* No change in structure is needed as it's already in the correct position. | ||
* | ||
* 3. basePath only | ||
* Before: basePath&i18n-locale=zh-CN | ||
* After: basePath?i18n-locale=zh-CN | ||
* Restructuring rationale: The '&' is changed to '?' because there were no existing | ||
* query parameters in the basePath. This ensures the locale is recognized as the | ||
* start of the query string rather than being misinterpreted as part of the path. | ||
* | ||
* The function performs the following steps: | ||
* 1. Extracts the locale parameter from its current position in the URL. | ||
* 2. Removes the locale parameter from its original position. | ||
* 3. Reconstructs the URL, placing the locale parameter in the correct position | ||
* based on the URL structure to ensure proper parsing by OpenSearch Dashboards. | ||
* 4. Updates the browser's URL without causing a page reload. | ||
* | ||
* @param {string} url - The full URL to process | ||
* @returns {string|null} The extracted locale value, or null if no locale was found | ||
*/ | ||
|
||
export function getAndUpdateLocaleInUrl(url: string): string | null { | ||
let fullUrl = ''; | ||
const { localeValue, localeParam, updatedUrl } = extractLocaleInfo(url); | ||
|
||
if (localeValue && localeParam) { | ||
const [basePath, hashPart] = updatedUrl.split('#'); | ||
|
||
if (hashPart !== undefined) { | ||
const [hashPath, hashQuery] = hashPart.split('?'); | ||
if (hashQuery) { | ||
// Category 2: basePath + # + hashPath + ? + hashQuery | ||
fullUrl = `${basePath}#${hashPath}?${hashQuery}&${localeParam}`; | ||
} else { | ||
// Category 1: basePath + # + hashPath (including when hashPath is '/' or empty) | ||
fullUrl = `${basePath}#${hashPath}?${localeParam}`; | ||
} | ||
} else { | ||
// Category 3: basePath only | ||
fullUrl = `${basePath}?${localeParam}`; | ||
} | ||
|
||
// Update the URL without causing a page reload | ||
window.history.replaceState(null, '', fullUrl); | ||
return localeValue; | ||
} | ||
|
||
return 'en'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.