Skip to content

Commit 2f2f384

Browse files
authored
Merge pull request #7802 from logto-io/charles-log-12192-log-12163-support-ui-locales
feat(core,experience,schemas): support ui_locales in Experience UI
2 parents 842bcd9 + 62275c6 commit 2f2f384

File tree

7 files changed

+25
-7
lines changed

7 files changed

+25
-7
lines changed

packages/core/src/middleware/koa-experience-ssr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default function koaExperienceSsr<StateT, ContextT extends WithI18nContex
4949
ctx,
5050
languageInfo: signInExperience.languageInfo,
5151
customLanguages,
52+
lng: typeof ctx.query.lng === 'string' ? ctx.query.lng : undefined,
5253
});
5354
const phrases = await libraries.phrases.getPhrases(language);
5455

packages/core/src/oidc/init.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export default function initOidc(
218218
removeUndefinedKeys({
219219
appId: typeof appId === 'string' ? appId : undefined,
220220
organizationId: params.organization_id,
221+
uiLocales: params.ui_locales,
221222
}) satisfies LogtoUiCookie
222223
),
223224
{ sameSite: 'lax', overwrite: true, httpOnly: false }

packages/core/src/utils/i18n.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const getExperienceLanguage = ({
3535
lng,
3636
}: GetExperienceLanguage) => {
3737
const acceptableLanguages = conditionalArray<string | string[]>(
38-
lng,
38+
lng?.split(/\s+/).filter(Boolean),
3939
autoDetect && detectLanguage(ctx),
4040
fallbackLanguage
4141
);

packages/experience/src/i18n/utils.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,24 @@ import i18next from 'i18next';
77
import LanguageDetector from 'i18next-browser-languagedetector';
88

99
import { getPhrases as getPhrasesApi } from '@/apis/settings';
10+
import { searchKeys } from '@/utils/search-parameters';
1011

1112
const getPhrases = async (language?: string) => {
12-
// Directly use the server-side phrases if it's already fetched
13-
if (isObject(logtoSsr) && (!language || logtoSsr.phrases.lng === language)) {
13+
const uiLocales = sessionStorage.getItem(searchKeys.uiLocales) ?? undefined;
14+
const uiLocalesFirst = uiLocales?.trim().split(/\s+/)[0];
15+
const preferredLanguage = language ?? uiLocales;
16+
17+
if (
18+
isObject(logtoSsr) &&
19+
(!preferredLanguage || logtoSsr.phrases.lng === (language ?? uiLocalesFirst))
20+
) {
1421
return { phrases: logtoSsr.phrases.data, lng: logtoSsr.phrases.lng };
1522
}
1623

1724
const detectedLanguage = detectLanguage();
1825
const response = await getPhrasesApi({
1926
localLanguage: Array.isArray(detectedLanguage) ? detectedLanguage.join(' ') : detectedLanguage,
20-
language,
27+
language: preferredLanguage,
2128
});
2229

2330
const remotePhrases = await response.json<LocalePhrase>();

packages/experience/src/utils/search-parameters.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { condString } from '@silverhand/essentials';
22

3-
export const searchKeysCamelCase = Object.freeze(['organizationId', 'appId'] as const);
3+
export const searchKeysCamelCase = Object.freeze(['organizationId', 'appId', 'uiLocales'] as const);
44

55
type SearchKeysCamelCase = (typeof searchKeysCamelCase)[number];
66

@@ -9,8 +9,15 @@ export const searchKeys = Object.freeze({
99
* The key for specifying the organization ID that may be used to override the default settings.
1010
*/
1111
organizationId: 'organization_id',
12-
/** The current application ID. */
12+
/**
13+
* The current application ID.
14+
*/
1315
appId: 'app_id',
16+
/**
17+
* The end-user's preferred languages, presented as a space-separated list of BCP47 language tags.
18+
* E.g. `en` or `en-US` or `en-US en`.
19+
*/
20+
uiLocales: 'ui_locales',
1421
} satisfies Record<SearchKeysCamelCase, string>);
1522

1623
export const handleSearchParametersData = () => {

packages/schemas/src/types/cookie.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { type ToZodObject } from '../utils/zod.js';
55
export type LogtoUiCookie = Partial<{
66
appId: string;
77
organizationId: string;
8+
uiLocales: string;
89
}>;
910

1011
export const logtoUiCookieGuard = z
11-
.object({ appId: z.string(), organizationId: z.string() })
12+
.object({ appId: z.string(), organizationId: z.string(), uiLocales: z.string() })
1213
.partial() satisfies ToZodObject<LogtoUiCookie>;

packages/schemas/src/types/ssr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type SsrData = {
99
signInExperience: {
1010
appId?: string;
1111
organizationId?: string;
12+
uiLocales?: string;
1213
data: FullSignInExperience;
1314
};
1415
phrases: {

0 commit comments

Comments
 (0)