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==}