Skip to content

Commit c44f03b

Browse files
feat(i18n): add peristant language preference sync for authenticated users (#3325)
* feat(i18n): add peristant language preference sync for authenticated users * feat(i18n): add user settings persistence configuration * feat(i18n): add language preference documentation * move user settings backend feature to new file * select defeault language based on the broswer language * add unit tests * make i18n optional config
1 parent 045656d commit c44f03b

File tree

14 files changed

+1717
-11
lines changed

14 files changed

+1717
-11
lines changed

docs/customization.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,5 +209,46 @@ i18n:
209209
- en
210210
- de
211211
- it
212-
defaultLocale: en # Optional. Defaults to `en` if not specified.
212+
defaultLocale: en # Optional. Used as fallback when browser language preferences don't match supported locales, or defaults to 'en' if not specified.
213213
```
214+
215+
### Default Language Selection Priority
216+
217+
Default language selection follows this priority order:
218+
219+
1. **Browser Language Priority**: The system first checks the user's browser language preferences to provide a personalized experience.
220+
221+
2. **Configuration Priority**: If no browser language matches the supported locales, the `defaultLocale` from the `i18n` configuration is used as a fallback.
222+
223+
3. **Fallback Priority**: If neither browser preferences nor configuration provide a match, defaults to `en`.
224+
225+
## Language Preferences
226+
227+
Red Hat Developer Hub automatically saves and restores user language settings across browser sessions. This feature is enabled by default and uses database storage.
228+
229+
### Configuration (Optional)
230+
231+
Language preferences use database storage by default. To opt-out and use browser storage instead, add the following to your `app-config.yaml`:
232+
233+
```yaml title="app-config.yaml"
234+
userSettings:
235+
persistence: browser # opt-out of database storage
236+
```
237+
238+
### Persistence Options
239+
240+
- **`database`** (default): Stores language preferences in the backend database. Persists across devices and browsers. No configuration required.
241+
- **`browser`** (opt-out): Stores language preferences in browser local storage. Limited to single browser/device.
242+
243+
## How It Works
244+
245+
When users change the language in the UI:
246+
247+
- The preference is automatically saved to storage
248+
- On next login/refresh, the language setting is restored
249+
- Guest users cannot persist language preferences
250+
251+
## Usage
252+
253+
1. Change language using any language selector in the UI
254+
2. Language setting will automatically be saved and restored

packages/app/config.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,15 @@ export interface Config {
285285
*/
286286
defaultLocale?: string;
287287
};
288+
/**
289+
* Configuration options for your user settings.
290+
* @deepVisibility frontend
291+
*/
292+
userSettings?: {
293+
/**
294+
* The persistence mode for user settings.
295+
* @visibility frontend
296+
*/
297+
persistence: 'browser' | 'database';
298+
};
288299
}

packages/app/src/apis.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
1-
import { OAuth2 } from '@backstage/core-app-api';
1+
import { OAuth2, WebStorage } from '@backstage/core-app-api';
22
import {
33
AnyApiFactory,
44
bitbucketAuthApiRef,
55
configApiRef,
66
createApiFactory,
77
discoveryApiRef,
8+
errorApiRef,
9+
fetchApiRef,
810
githubAuthApiRef,
911
gitlabAuthApiRef,
1012
identityApiRef,
1113
microsoftAuthApiRef,
1214
oauthRequestApiRef,
15+
storageApiRef,
1316
} from '@backstage/core-plugin-api';
1417
import {
1518
ScmAuth,
1619
scmAuthApiRef,
1720
ScmIntegrationsApi,
1821
scmIntegrationsApiRef,
1922
} from '@backstage/integration-react';
23+
import { UserSettingsStorage } from '@backstage/plugin-user-settings';
2024

2125
import {
2226
auth0AuthApiRef,
@@ -29,6 +33,24 @@ import {
2933
} from './api/LearningPathApiClient';
3034

3135
export const apis: AnyApiFactory[] = [
36+
createApiFactory({
37+
api: storageApiRef,
38+
deps: {
39+
discoveryApi: discoveryApiRef,
40+
errorApi: errorApiRef,
41+
fetchApi: fetchApiRef,
42+
identityApi: identityApiRef,
43+
configApi: configApiRef,
44+
},
45+
factory: deps => {
46+
const persistence =
47+
deps.configApi.getOptionalString('userSettings.persistence') ??
48+
'database';
49+
return persistence === 'browser'
50+
? WebStorage.create(deps)
51+
: UserSettingsStorage.create(deps);
52+
},
53+
}),
3254
createApiFactory({
3355
api: scmIntegrationsApiRef,
3456
deps: { configApi: configApiRef },

packages/app/src/components/DynamicRoot/DynamicRoot.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import extractDynamicConfig, {
4343
DynamicRoute,
4444
} from '../../utils/dynamicUI/extractDynamicConfig';
4545
import initializeRemotePlugins from '../../utils/dynamicUI/initializeRemotePlugins';
46+
import { getDefaultLanguage } from '../../utils/language/language';
4647
import { catalogTranslations } from '../catalog/translations/catalog';
4748
import { MenuIcon } from '../Root/MenuIcon';
4849
import CommonIcons from './CommonIcons';
@@ -563,7 +564,7 @@ export const DynamicRoot = ({
563564
app.current = createApp({
564565
__experimentalTranslations: {
565566
availableLanguages: translationConfig?.locales ?? ['en'],
566-
defaultLanguage: translationConfig?.defaultLocale,
567+
defaultLanguage: getDefaultLanguage(translationConfig),
567568
resources: [
568569
catalogTranslations,
569570
scaffolderTranslations,

packages/app/src/components/Root/Root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import DynamicRootContext, {
4040
ResolvedMenuItem,
4141
} from '@red-hat-developer-hub/plugin-utils';
4242

43+
import { useLanguagePreference } from '../../hooks/useLanguagePreference';
4344
import { ApplicationHeaders } from './ApplicationHeaders';
4445
import { MenuIcon } from './MenuIcon';
4546
import { SidebarLogo } from './SidebarLogo';
@@ -297,6 +298,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => {
297298
permission: policyEntityCreatePermission,
298299
resourceRef: undefined,
299300
});
301+
useLanguagePreference();
300302

301303
const handleClick = (itemName: string) => {
302304
setOpenItems(prevOpenItems => ({

0 commit comments

Comments
 (0)