Skip to content

Commit f4f46e6

Browse files
authored
feat(account-center): setup i18n locales (#7973)
1 parent 75982ee commit f4f46e6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+601
-15
lines changed

packages/account-center/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,27 @@
2121
},
2222
"devDependencies": {
2323
"@logto/core-kit": "workspace:^",
24+
"@logto/language-kit": "workspace:^",
25+
"@logto/phrases-account-center": "workspace:^",
2426
"@logto/react": "^4.0.6",
2527
"@logto/schemas": "workspace:^",
28+
"@silverhand/essentials": "^2.9.1",
2629
"@silverhand/eslint-config": "6.0.1",
2730
"@silverhand/eslint-config-react": "6.0.2",
2831
"@silverhand/ts-config": "6.0.0",
2932
"@silverhand/ts-config-react": "6.0.0",
3033
"@types/react": "^18.3.3",
3134
"@types/react-dom": "^18.3.0",
3235
"@vitejs/plugin-react": "^4.3.1",
33-
"ky": "^1.2.3",
3436
"eslint": "^8.56.0",
37+
"i18next": "^22.4.15",
38+
"i18next-browser-languagedetector": "^8.0.0",
39+
"ky": "^1.2.3",
3540
"lint-staged": "^15.0.0",
3641
"prettier": "^3.5.3",
37-
"stylelint": "^15.0.0",
3842
"react": "^18.3.1",
43+
"stylelint": "^15.0.0",
44+
"react-i18next": "^12.3.1",
3945
"react-dom": "^18.3.1",
4046
"typescript": "^5.5.3",
4147
"vite": "^6.3.6",

packages/account-center/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import Callback from './Callback';
77
import PageContextProvider from './Providers/PageContextProvider';
88
import PageContext from './Providers/PageContextProvider/PageContext';
99
import BrandingHeader from './components/BrandingHeader';
10+
import initI18n from './i18n/init';
1011

1112
import '@/scss/normalized.scss';
1213

14+
void initI18n();
15+
1316
const redirectUri = `${window.location.origin}/account-center`;
1417

1518
const Main = () => {

packages/account-center/src/components/BrandingHeader/index.module.scss

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@
55
align-items: center;
66
padding: _.unit(4) _.unit(6);
77
min-height: _.unit(16);
8-
}
8+
gap: _.unit(4);
9+
10+
.logo {
11+
max-height: _.unit(10);
12+
width: auto;
13+
}
14+
15+
.splitter {
16+
height: 16px;
17+
width: 1px;
18+
background-color: var(--color-border);
19+
}
920

10-
.logo {
11-
max-height: _.unit(10);
12-
width: auto;
21+
.title {
22+
font: var(--font-title-1);
23+
color: var(--color-text-strong);
24+
}
1325
}

packages/account-center/src/components/BrandingHeader/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useContext } from 'react';
2+
import { useTranslation } from 'react-i18next';
23

34
import PageContext from '@/Providers/PageContextProvider/PageContext';
45
import { getBrandingLogoUrl } from '@/utils/logo';
@@ -7,6 +8,7 @@ import styles from './index.module.scss';
78

89
const BrandingHeader = () => {
910
const { theme, experienceSettings } = useContext(PageContext);
11+
const { t } = useTranslation();
1012

1113
if (!experienceSettings) {
1214
return null;
@@ -25,6 +27,8 @@ const BrandingHeader = () => {
2527
return (
2628
<header className={styles.header}>
2729
<img className={styles.logo} src={logoUrl} alt="logo" />
30+
<div className={styles.splitter} />
31+
<span className={styles.title}>{t('header.title')}</span>
2832
</header>
2933
);
3034
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { LanguageTag } from '@logto/language-kit';
2+
import resources from '@logto/phrases-account-center';
3+
import i18next from 'i18next';
4+
import LanguageDetector from 'i18next-browser-languagedetector';
5+
import { initReactI18next } from 'react-i18next';
6+
7+
import { resolveLanguage, storageKey } from '@/i18n/utils';
8+
9+
i18next.use(initReactI18next).use(LanguageDetector);
10+
11+
const initI18n = async (initialLanguage?: LanguageTag) => {
12+
const normalizedLanguage =
13+
typeof initialLanguage === 'string' ? resolveLanguage(initialLanguage) : undefined;
14+
15+
await i18next.init({
16+
resources: {},
17+
fallbackLng: 'en',
18+
lng: normalizedLanguage,
19+
detection: {
20+
lookupLocalStorage: storageKey,
21+
lookupSessionStorage: storageKey,
22+
},
23+
interpolation: {
24+
escapeValue: false,
25+
},
26+
});
27+
28+
for (const [language, value] of Object.entries(resources)) {
29+
i18next.addResourceBundle(language, 'translation', value.translation, true);
30+
}
31+
};
32+
33+
export default initI18n;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { isBuiltInLanguageTag } from '@logto/phrases-account-center';
2+
import type { LanguageInfo } from '@logto/schemas';
3+
import i18next from 'i18next';
4+
import LanguageDetector from 'i18next-browser-languagedetector';
5+
6+
const storageKey = 'i18nextAccountCenterLng';
7+
export { storageKey };
8+
9+
export const resolveLanguage = (language?: string) =>
10+
language && isBuiltInLanguageTag(language) ? language : undefined;
11+
12+
export const detectLanguage = (languageSettings?: LanguageInfo) => {
13+
if (languageSettings?.autoDetect === false) {
14+
return resolveLanguage(languageSettings.fallbackLanguage) ?? 'en';
15+
}
16+
17+
const languageDetector = new LanguageDetector();
18+
languageDetector.init(
19+
{ languageUtils: {} },
20+
{
21+
lookupLocalStorage: storageKey,
22+
lookupSessionStorage: storageKey,
23+
}
24+
);
25+
26+
const detected = languageDetector.detect();
27+
28+
if (Array.isArray(detected)) {
29+
const matched = detected.find((language) => isBuiltInLanguageTag(language));
30+
return matched ?? 'en';
31+
}
32+
33+
return resolveLanguage(detected) ?? 'en';
34+
};
35+
36+
export const changeLanguage = async (language: string) => {
37+
await i18next.changeLanguage(resolveLanguage(language) ?? 'en');
38+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// https://react.i18next.com/latest/typescript#create-a-declaration-file
2+
3+
import type { LocalePhrase } from '@logto/phrases-account-center';
4+
5+
declare module 'i18next' {
6+
interface CustomTypeOptions {
7+
resources: LocalePhrase;
8+
}
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# `@logto/phrases-account-center`
2+
3+
See [the main README](../../README.md) for more information.
4+
5+
## Caveats
6+
7+
Dots (`.`) are not allowed in the phrase keys. Despite i18next supporting them, they may cause issues when customizing the phrases in the Console.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"name": "@logto/phrases-account-center",
3+
"version": "0.1.0",
4+
"description": "Logto shared phrases (i18n) for account center.",
5+
"author": "Silverhand Inc. <[email protected]>",
6+
"homepage": "https://github.com/logto-io/logto#readme",
7+
"license": "MPL-2.0",
8+
"type": "module",
9+
"main": "lib/index.js",
10+
"publishConfig": {
11+
"access": "public"
12+
},
13+
"directories": {
14+
"lib": "lib"
15+
},
16+
"files": [
17+
"lib"
18+
],
19+
"repository": {
20+
"type": "git",
21+
"url": "git+https://github.com/logto-io/logto.git"
22+
},
23+
"scripts": {
24+
"precommit": "lint-staged",
25+
"build": "rm -rf lib/ && tsc",
26+
"build:test": "pnpm build",
27+
"dev": "tsc --watch --preserveWatchOutput --incremental",
28+
"lint": "eslint --ext .ts src",
29+
"lint:report": "pnpm lint --format json --output-file report.json",
30+
"prepack": "pnpm build"
31+
},
32+
"bugs": {
33+
"url": "https://github.com/logto-io/logto/issues"
34+
},
35+
"dependencies": {
36+
"@logto/core-kit": "workspace:^",
37+
"@logto/language-kit": "workspace:^",
38+
"@silverhand/essentials": "^2.9.1"
39+
},
40+
"peerDependencies": {
41+
"zod": "3.24.3"
42+
},
43+
"devDependencies": {
44+
"@silverhand/eslint-config": "6.0.1",
45+
"@silverhand/ts-config": "6.0.0",
46+
"eslint": "^8.56.0",
47+
"lint-staged": "^15.0.0",
48+
"prettier": "^3.5.3",
49+
"typescript": "^5.5.3"
50+
},
51+
"engines": {
52+
"node": "^22.14.0"
53+
},
54+
"eslintConfig": {
55+
"extends": "@silverhand"
56+
},
57+
"prettier": "@silverhand/eslint-config/.prettierrc"
58+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { LanguageTag } from '@logto/language-kit';
2+
import { fallback, languages } from '@logto/language-kit';
3+
import type { NormalizeKeyPaths } from '@silverhand/essentials';
4+
import { z } from 'zod';
5+
6+
import ar from './locales/ar/index.js';
7+
import de from './locales/de/index.js';
8+
import en from './locales/en/index.js';
9+
import es from './locales/es/index.js';
10+
import fr from './locales/fr/index.js';
11+
import it from './locales/it/index.js';
12+
import ja from './locales/ja/index.js';
13+
import ko from './locales/ko/index.js';
14+
import plPL from './locales/pl-pl/index.js';
15+
import ptBR from './locales/pt-br/index.js';
16+
import ptPT from './locales/pt-pt/index.js';
17+
import ru from './locales/ru/index.js';
18+
import th from './locales/th/index.js';
19+
import trTR from './locales/tr-tr/index.js';
20+
import ukUA from './locales/uk-ua/index.js';
21+
import zhCN from './locales/zh-cn/index.js';
22+
import zhHK from './locales/zh-hk/index.js';
23+
import zhTW from './locales/zh-tw/index.js';
24+
import type { LocalePhrase } from './types.js';
25+
26+
export type { LocalePhrase } from './types.js';
27+
28+
export type I18nKey = NormalizeKeyPaths<typeof en.translation>;
29+
30+
export const builtInLanguages = [
31+
'ar',
32+
'de',
33+
'en',
34+
'es',
35+
'fr',
36+
'it',
37+
'ja',
38+
'ko',
39+
'pl-PL',
40+
'pt-PT',
41+
'pt-BR',
42+
'ru',
43+
'th',
44+
'tr-TR',
45+
'uk-UA',
46+
'zh-CN',
47+
'zh-HK',
48+
'zh-TW',
49+
] as const;
50+
51+
export const builtInLanguageOptions = builtInLanguages.map((languageTag) => ({
52+
value: languageTag,
53+
title: languages[languageTag],
54+
}));
55+
56+
export const builtInLanguageTagGuard = z.enum(builtInLanguages);
57+
58+
export type BuiltInLanguageTag = z.infer<typeof builtInLanguageTagGuard>;
59+
60+
export type Resource = Record<BuiltInLanguageTag, LocalePhrase>;
61+
62+
const resource: Resource = {
63+
ar,
64+
de,
65+
en,
66+
es,
67+
fr,
68+
it,
69+
ja,
70+
ko,
71+
'pl-PL': plPL,
72+
'pt-PT': ptPT,
73+
'pt-BR': ptBR,
74+
ru,
75+
th,
76+
'tr-TR': trTR,
77+
'uk-UA': ukUA,
78+
'zh-CN': zhCN,
79+
'zh-HK': zhHK,
80+
'zh-TW': zhTW,
81+
};
82+
83+
export const getDefaultLanguageTag = (language: string): LanguageTag =>
84+
builtInLanguageTagGuard.or(fallback<LanguageTag>('en')).parse(language);
85+
86+
export const isBuiltInLanguageTag = (language: string): language is BuiltInLanguageTag =>
87+
builtInLanguageTagGuard.safeParse(language).success;
88+
89+
export default resource;

0 commit comments

Comments
 (0)