diff --git a/packages/console/src/i18n/init.ts b/packages/console/src/i18n/init.ts index 2fec238f9..aa65cd7aa 100644 --- a/packages/console/src/i18n/init.ts +++ b/packages/console/src/i18n/init.ts @@ -1,10 +1,10 @@ -import type { LanguageKey } from '@logto/core-kit'; +import { LanguageTag } from '@logto/language-kit'; import resources from '@logto/phrases'; import i18next from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import { initReactI18next } from 'react-i18next'; -const initI18n = async (language?: LanguageKey) => +const initI18n = async (language?: LanguageTag) => i18next .use(initReactI18next) .use(LanguageDetector) diff --git a/packages/console/src/include.d/react-i18next.d.ts b/packages/console/src/include.d/react-i18next.d.ts index 727283636..119678c7a 100644 --- a/packages/console/src/include.d/react-i18next.d.ts +++ b/packages/console/src/include.d/react-i18next.d.ts @@ -1,13 +1,10 @@ // https://react.i18next.com/latest/typescript#create-a-declaration-file -import { Translation, Errors } from '@logto/phrases'; +import { LocalPhrase } from '@logto/phrases'; declare module 'react-i18next' { interface CustomTypeOptions { allowObjectInHTMLChildren: true; - resources: { - translation: Translation; - errors: Errors; - }; + resources: LocalPhrase; } } diff --git a/packages/console/src/pages/Settings/index.tsx b/packages/console/src/pages/Settings/index.tsx index b62eb39ab..616ee85d2 100644 --- a/packages/console/src/pages/Settings/index.tsx +++ b/packages/console/src/pages/Settings/index.tsx @@ -1,5 +1,7 @@ -import { getDefaultLanguage } from '@logto/core-kit'; -import { languageOptions } from '@logto/phrases'; +import { + builtInLanguageOptions as consoleBuiltInLanguageOptions, + getDefaultLanguageTag, +} from '@logto/phrases'; import { AppearanceMode } from '@logto/schemas'; import classNames from 'classnames'; import { Controller, useForm } from 'react-hook-form'; @@ -25,7 +27,7 @@ const Settings = () => { i18n: { language }, } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const defaultLanguage = getDefaultLanguage(language); + const defaultLanguage = getDefaultLanguageTag(language); const { data, error, update, isLoading, isLoaded } = useUserPreferences(); const { @@ -63,7 +65,7 @@ const Settings = () => { render={({ field: { value, onChange } }) => ( <Select value={value ?? defaultLanguage} - options={languageOptions} + options={consoleBuiltInLanguageOptions} onChange={onChange} /> )} diff --git a/packages/demo-app/package.json b/packages/demo-app/package.json index b5d348f0f..bdaa57787 100644 --- a/packages/demo-app/package.json +++ b/packages/demo-app/package.json @@ -17,10 +17,11 @@ "stylelint": "stylelint \"src/**/*.scss\"" }, "devDependencies": { + "@logto/core-kit": "^1.0.0-beta.13", + "@logto/language-kit": "1.0.0-beta.16", "@logto/phrases": "^1.0.0-beta.9", "@logto/react": "1.0.0-beta.8", "@logto/schemas": "^1.0.0-beta.9", - "@logto/core-kit": "^1.0.0-beta.13", "@parcel/core": "2.7.0", "@parcel/transformer-sass": "2.7.0", "@silverhand/eslint-config": "1.0.0", diff --git a/packages/demo-app/src/i18n/init.ts b/packages/demo-app/src/i18n/init.ts index b9fcb9546..df9f8850c 100644 --- a/packages/demo-app/src/i18n/init.ts +++ b/packages/demo-app/src/i18n/init.ts @@ -1,10 +1,10 @@ -import type { LanguageKey } from '@logto/core-kit'; +import type { LanguageTag } from '@logto/language-kit'; import resources from '@logto/phrases'; import i18next from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import { initReactI18next } from 'react-i18next'; -const initI18n = async (language?: LanguageKey) => +const initI18n = async (language?: LanguageTag) => i18next .use(initReactI18next) .use(LanguageDetector) diff --git a/packages/demo-app/src/include.d/react-i18next.d.ts b/packages/demo-app/src/include.d/react-i18next.d.ts index 204b95f7f..9f69cc137 100644 --- a/packages/demo-app/src/include.d/react-i18next.d.ts +++ b/packages/demo-app/src/include.d/react-i18next.d.ts @@ -1,15 +1,12 @@ // https://react.i18next.com/latest/typescript#create-a-declaration-file -import { Translation, Errors } from '@logto/phrases'; +import { LocalPhrase } from '@logto/phrases'; // eslint-disable-next-line unused-imports/no-unused-imports import { CustomTypeOptions } from 'react-i18next'; declare module 'react-i18next' { interface CustomTypeOptions { allowObjectInHTMLChildren: true; - resources: { - translation: Translation; - errors: Errors; - }; + resources: LocalPhrase; } } diff --git a/packages/phrases/package.json b/packages/phrases/package.json index 3c6daf68b..6a854308e 100644 --- a/packages/phrases/package.json +++ b/packages/phrases/package.json @@ -29,9 +29,10 @@ "url": "https://github.com/logto-io/logto/issues" }, "dependencies": { - "@logto/core-kit": "^1.0.0-beta.13", + "@logto/core-kit": "1.0.0-beta.16", "@logto/language-kit": "1.0.0-beta.15", - "@silverhand/essentials": "^1.2.1" + "@silverhand/essentials": "^1.2.1", + "zod": "^3.18.0" }, "devDependencies": { "@silverhand/eslint-config": "1.0.0", diff --git a/packages/phrases/src/index.ts b/packages/phrases/src/index.ts index 9d23593d7..d8f3b948e 100644 --- a/packages/phrases/src/index.ts +++ b/packages/phrases/src/index.ts @@ -1,4 +1,7 @@ +import { fallback } from '@logto/core-kit'; +import { languages, LanguageTag } from '@logto/language-kit'; import { NormalizeKeyPaths } from '@silverhand/essentials'; +import { z } from 'zod'; import en from './locales/en'; import fr from './locales/fr'; @@ -6,16 +9,37 @@ import koKR from './locales/ko-kr'; import ptPT from './locales/pt-pt'; import trTR from './locales/tr-tr'; import zhCN from './locales/zh-cn'; -import { Resource } from './types'; +import { LocalPhrase } from './types'; + +export type { LocalPhrase } from './types'; + +export type I18nKey = NormalizeKeyPaths<typeof en.translation>; + +export const builtInLanguages = ['en', 'fr', 'pt-PT', 'zh-CN', 'ko-KR', 'tr-TR'] as const; + +export const builtInLanguageOptions = builtInLanguages.map((languageTag) => ({ + value: languageTag, + title: languages[languageTag], +})); + +export const builtInLanguageTagGuard = z.enum(builtInLanguages); + +export type BuiltInLanguageTag = z.infer<typeof builtInLanguageTagGuard>; -export { languageOptions } from './types'; -export type Translation = typeof en.translation; export type Errors = typeof en.errors; export type LogtoErrorCode = NormalizeKeyPaths<Errors>; export type LogtoErrorI18nKey = `errors:${LogtoErrorCode}`; -export type I18nKey = NormalizeKeyPaths<Translation>; + export type AdminConsoleKey = NormalizeKeyPaths<typeof en.translation.admin_console>; +export const getDefaultLanguageTag = (languages: string): LanguageTag => + builtInLanguageTagGuard.or(fallback<LanguageTag>('en')).parse(languages); + +export const isBuiltInLanguageTag = (language: string): language is BuiltInLanguageTag => + builtInLanguageTagGuard.safeParse(language).success; + +export type Resource = Record<BuiltInLanguageTag, LocalPhrase>; + const resource: Resource = { en, fr, diff --git a/packages/phrases/src/locales/fr/index.ts b/packages/phrases/src/locales/fr/index.ts index 79da482a7..f2dbf4643 100644 --- a/packages/phrases/src/locales/fr/index.ts +++ b/packages/phrases/src/locales/fr/index.ts @@ -1,8 +1,8 @@ -import en from '../en'; +import { LocalPhrase } from '../../types'; import errors from './errors'; import translation from './translation'; -const fr: typeof en = Object.freeze({ +const fr: LocalPhrase = Object.freeze({ translation, errors, }); diff --git a/packages/phrases/src/locales/ko-kr/index.ts b/packages/phrases/src/locales/ko-kr/index.ts index 144190b6a..0cc717165 100644 --- a/packages/phrases/src/locales/ko-kr/index.ts +++ b/packages/phrases/src/locales/ko-kr/index.ts @@ -1,8 +1,8 @@ -import en from '../en'; +import { LocalPhrase } from '../../types'; import errors from './errors'; import translation from './translation'; -const koKR: typeof en = Object.freeze({ +const koKR: LocalPhrase = Object.freeze({ translation, errors, }); diff --git a/packages/phrases/src/locales/pt-pt/index.ts b/packages/phrases/src/locales/pt-pt/index.ts index be517acf3..d05767c78 100644 --- a/packages/phrases/src/locales/pt-pt/index.ts +++ b/packages/phrases/src/locales/pt-pt/index.ts @@ -1,8 +1,8 @@ -import en from '../en'; +import { LocalPhrase } from '../../types'; import errors from './errors'; import translation from './translation'; -const ptPT: typeof en = Object.freeze({ +const ptPT: LocalPhrase = Object.freeze({ translation, errors, }); diff --git a/packages/phrases/src/locales/tr-tr/index.ts b/packages/phrases/src/locales/tr-tr/index.ts index a515674fa..d03b5324f 100644 --- a/packages/phrases/src/locales/tr-tr/index.ts +++ b/packages/phrases/src/locales/tr-tr/index.ts @@ -1,8 +1,8 @@ -import en from '../en'; +import { LocalPhrase } from '../../types'; import errors from './errors'; import translation from './translation'; -const trTR: typeof en = Object.freeze({ +const trTR: LocalPhrase = Object.freeze({ translation, errors, }); diff --git a/packages/phrases/src/locales/zh-cn/index.ts b/packages/phrases/src/locales/zh-cn/index.ts index a3d61151c..b1bf96f33 100644 --- a/packages/phrases/src/locales/zh-cn/index.ts +++ b/packages/phrases/src/locales/zh-cn/index.ts @@ -1,8 +1,8 @@ -import en from '../en'; +import { LocalPhrase } from '../../types'; import errors from './errors'; import translation from './translation'; -const zhCN: typeof en = Object.freeze({ +const zhCN: LocalPhrase = Object.freeze({ translation, errors, }); diff --git a/packages/phrases/src/types.ts b/packages/phrases/src/types.ts index 12e79dd92..d8cab9855 100644 --- a/packages/phrases/src/types.ts +++ b/packages/phrases/src/types.ts @@ -1,21 +1,3 @@ -import { LanguageKey, languageKeyGuard } from '@logto/core-kit'; +import en from './locales/en'; -/* Copied from i18next/index.d.ts */ -export type Resource = Record<LanguageKey, ResourceLanguage>; - -export type ResourceLanguage = Record<string, ResourceKey>; - -export type ResourceKey = string | Record<string, unknown>; - -const languageCodeAndDisplayNameMappings: Record<LanguageKey, string> = { - en: 'English', - fr: 'Français', - 'pt-PT': 'Português', - 'zh-CN': '简体中文', - 'tr-TR': 'Türkçe', - 'ko-KR': '한국어', -}; - -export const languageOptions: Array<{ value: LanguageKey; title: string }> = Object.entries( - languageCodeAndDisplayNameMappings -).map(([key, value]) => ({ value: languageKeyGuard.parse(key), title: value })); +export type LocalPhrase = typeof en; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8b97356d..afe8beb4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -318,6 +318,7 @@ importers: packages/demo-app: specifiers: '@logto/core-kit': ^1.0.0-beta.13 + '@logto/language-kit': 1.0.0-beta.16 '@logto/phrases': ^1.0.0-beta.9 '@logto/react': 1.0.0-beta.8 '@logto/schemas': ^1.0.0-beta.9 @@ -344,6 +345,7 @@ importers: typescript: ^4.7.4 devDependencies: '@logto/core-kit': 1.0.0-beta.13 + '@logto/language-kit': 1.0.0-beta.16 '@logto/phrases': link:../phrases '@logto/react': 1.0.0-beta.8_react@18.2.0 '@logto/schemas': link:../schemas @@ -423,7 +425,7 @@ importers: packages/phrases: specifiers: - '@logto/core-kit': ^1.0.0-beta.13 + '@logto/core-kit': 1.0.0-beta.16 '@logto/language-kit': 1.0.0-beta.15 '@silverhand/eslint-config': 1.0.0 '@silverhand/essentials': ^1.2.1 @@ -432,10 +434,12 @@ importers: lint-staged: ^13.0.0 prettier: ^2.7.1 typescript: ^4.7.4 + zod: ^3.18.0 dependencies: - '@logto/core-kit': 1.0.0-beta.13 + '@logto/core-kit': 1.0.0-beta.16 '@logto/language-kit': 1.0.0-beta.15 '@silverhand/essentials': 1.2.1 + zod: 3.18.0 devDependencies: '@silverhand/eslint-config': 1.0.0_swk2g7ygmfleszo5c33j4vooni '@silverhand/ts-config': 1.0.0_typescript@4.7.4 @@ -2465,7 +2469,7 @@ packages: dependencies: '@logto/language-kit': 1.0.0-beta.16 color: 4.2.3 - nanoid: 3.1.30 + nanoid: 3.3.4 zod: 3.18.0 /@logto/js/1.0.0-beta.8: @@ -11288,6 +11292,7 @@ packages: resolution: {integrity: sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + dev: false /nanoid/3.3.1: resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==}