Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the locale dynamically by adding &i18n-locale to URL #7686

Merged
merged 5 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- public
- application
- [Hooks](../src/plugins/console/public/application/hooks/README.md)
- [Content_management](../src/plugins/content_management/README.md)
- [Csp_handler](../src/plugins/csp_handler/README.md)
- [Dashboard](../src/plugins/dashboard/README.md)
- [Data](../src/plugins/data/README.md)
Expand Down Expand Up @@ -164,6 +165,7 @@
- [Opensearch dashboards.release notes 2.13.0](../release-notes/opensearch-dashboards.release-notes-2.13.0.md)
- [Opensearch dashboards.release notes 2.14.0](../release-notes/opensearch-dashboards.release-notes-2.14.0.md)
- [Opensearch dashboards.release notes 2.15.0](../release-notes/opensearch-dashboards.release-notes-2.15.0.md)
- [Opensearch dashboards.release notes 2.16.0](../release-notes/opensearch-dashboards.release-notes-2.16.0.md)
- [Opensearch dashboards.release notes 2.2.0](../release-notes/opensearch-dashboards.release-notes-2.2.0.md)
- [Opensearch dashboards.release notes 2.2.1](../release-notes/opensearch-dashboards.release-notes-2.2.1.md)
- [Opensearch dashboards.release notes 2.3.0](../release-notes/opensearch-dashboards.release-notes-2.3.0.md)
Expand Down
9 changes: 8 additions & 1 deletion packages/osd-i18n/src/core/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,5 +261,12 @@ export async function load(translationsUrl: string) {
throw new Error(`Translations request failed with status code: ${response.status}`);
}

init(await response.json());
const data = await response.json();

if (data.warning) {
// Store the warning to be displayed after core system setup
(window as any).__i18nWarning = data.warning;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it common to store this in window?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is not uncommon, especially in situations where we need to pass information from sever side to browser that doesn't have a direct communication channel.

}

init(data.translations);
}
4 changes: 2 additions & 2 deletions src/core/public/application/scoped_history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
Action,
} from 'history';
import { i18n } from '@osd/i18n';
import { extractLocaleInfo } from '../locale_helper';
import { getLocaleInUrl } from '../locale_helper';

/**
* A wrapper around a `History` instance that is scoped to a particular base path of the history stack. Behaves
Expand Down Expand Up @@ -318,7 +318,7 @@ export class ScopedHistory<HistoryLocationState = unknown>
return;
}
// const fullUrl = `${location.pathname}${location.search}${location.hash}`;
const { localeValue } = extractLocaleInfo(window.location.href);
const localeValue = getLocaleInUrl(window.location.href);
if (localeValue !== currentLocale) {
// Force a full page reload
window.location.reload();
Expand Down
31 changes: 31 additions & 0 deletions src/core/public/core_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,35 @@
this.workspaces.stop();
this.rootDomElement.textContent = '';
}

public async displayWarning(title: string, text: string) {
const i18n = await this.i18n.start();
const injectedMetadata = this.injectedMetadata.setup();
this.fatalErrorsSetup = this.fatalErrors.setup({
injectedMetadata,
i18n: this.i18n.getContext(),
});
await this.integrations.setup();

Check failure on line 329 in src/core/public/core_system.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

Delete `··`
this.docLinks.setup();

Check failure on line 330 in src/core/public/core_system.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

Delete `··`
const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });

Check failure on line 331 in src/core/public/core_system.ts

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

Delete `··`
const uiSettings = this.uiSettings.setup({ http, injectedMetadata });
const notificationsTargetDomElement = document.createElement('div');
const overlayTargetDomElement = document.createElement('div');
const overlays = this.overlay.start({
i18n,
targetDomElement: overlayTargetDomElement,
uiSettings,
});
if (this.notifications) {
const toasts = await this.notifications.start({
i18n,
overlays,
targetDomElement: notificationsTargetDomElement,
})?.toasts;

if (toasts) {
toasts.addWarning({ title, text });
}
}
}
}
144 changes: 33 additions & 111 deletions src/core/public/locale_helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,126 +3,48 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { extractLocaleInfo, getAndUpdateLocaleInUrl } from './locale_helper';
import { getLocaleInUrl } 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=(...)',
},
},
];
describe('getLocaleInUrl', () => {
beforeEach(() => {
// Clear any warnings before each test
delete (window as any).__localeWarning;
});

testCases.forEach(({ description, input, expected }) => {
it(description, () => {
const result = extractLocaleInfo(input);
expect(result).toEqual(expected);
});
it('should return the locale from a valid query string', () => {
const url = 'http://localhost:5603/app/home?locale=en-US';
expect(getLocaleInUrl(url)).toBe('en-US');
});

it('should return the locale from a valid hash query string', () => {
const url = 'http://localhost:5603/app/home#/?locale=fr-FR';
expect(getLocaleInUrl(url)).toBe('fr-FR');
});
});

describe('getAndUpdateLocaleInUrl', () => {
let originalHistoryReplaceState: typeof window.history.replaceState;
it('should return null for a URL without locale', () => {
const url = 'http://localhost:5603/app/home';
expect(getLocaleInUrl(url)).toBeNull();
});

beforeEach(() => {
// Mock window.history.replaceState
originalHistoryReplaceState = window.history.replaceState;
window.history.replaceState = jest.fn();
it('should return null and set a warning for an invalid locale format in hash', () => {
const url = 'http://localhost:5603/app/home#/&locale=de-DE';
expect(getLocaleInUrl(url)).toBeNull();
expect((window as any).__localeWarning).toBeDefined();
expect((window as any).__localeWarning.title).toBe('Invalid URL Format');
});

afterEach(() => {
// Restore original window.history.replaceState
window.history.replaceState = originalHistoryReplaceState;
it('should return null for an empty locale value', () => {
const url = 'http://localhost:5603/app/home?locale=';
expect(getLocaleInUrl(url)).toBeNull();
});

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',
},
];
it('should handle URLs with other query parameters', () => {
const url = 'http://localhost:5603/app/home?param1=value1&locale=ja-JP&param2=value2';
expect(getLocaleInUrl(url)).toBe('ja-JP');
});

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();
}
});
it('should handle URLs with other hash parameters', () => {
const url = 'http://localhost:5603/app/home#/route?param1=value1&locale=zh-CN&param2=value2';
expect(getLocaleInUrl(url)).toBe('zh-CN');
});
});
Loading
Loading