alepha 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/jobs/index.d.ts +20 -20
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/keys/index.d.ts +6 -6
- package/dist/api/users/index.d.ts +43 -9
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +24 -3
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +13 -13
- package/dist/cli/core/index.d.ts +46 -40
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +51 -101
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/i18n/index.d.ts +12 -5
- package/dist/cli/i18n/index.d.ts.map +1 -1
- package/dist/cli/i18n/index.js +45 -11
- package/dist/cli/i18n/index.js.map +1 -1
- package/dist/cli/platform-lib/index.d.ts +32 -6
- package/dist/cli/platform-lib/index.d.ts.map +1 -1
- package/dist/cli/platform-lib/index.js +82 -19
- package/dist/cli/platform-lib/index.js.map +1 -1
- package/dist/command/index.d.ts +1 -1
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +23 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/react/form/index.d.ts +0 -1
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +16 -15
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +43 -0
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +114 -10
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/router/index.browser.js +128 -5
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +108 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +184 -6
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/sitemap/index.browser.js +35 -0
- package/dist/react/sitemap/index.browser.js.map +1 -0
- package/dist/react/sitemap/index.d.ts +92 -0
- package/dist/react/sitemap/index.d.ts.map +1 -0
- package/dist/react/sitemap/index.js +131 -0
- package/dist/react/sitemap/index.js.map +1 -0
- package/dist/server/auth/index.d.ts +105 -1
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1604 -7
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.d.ts +15 -0
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +22 -3
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +18 -0
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +25 -0
- package/dist/server/core/index.js.map +1 -1
- package/package.json +16 -3
- package/src/api/users/controllers/RealmController.ts +1 -0
- package/src/api/users/primitives/$realm.ts +26 -0
- package/src/api/users/providers/RealmProvider.ts +15 -0
- package/src/api/users/schemas/realmConfigSchema.ts +14 -0
- package/src/cli/core/atoms/buildOptions.ts +0 -12
- package/src/cli/core/commands/build.ts +0 -10
- package/src/cli/core/index.ts +0 -3
- package/src/cli/core/tasks/BuildCloudflareTask.ts +37 -17
- package/src/cli/core/tasks/BuildPrerenderTask.ts +44 -7
- package/src/cli/i18n/__tests__/I18nCheckService.spec.ts +48 -0
- package/src/cli/i18n/services/I18nCheckService.ts +65 -11
- package/src/cli/platform-lib/adapters/CloudflareAdapter.ts +128 -36
- package/src/mcp/__tests__/McpServerProvider.spec.ts +71 -0
- package/src/mcp/providers/McpServerProvider.ts +55 -0
- package/src/react/form/__tests__/FormModel-submit-loading.spec.ts +71 -0
- package/src/react/form/__tests__/form-submitting-reactive.browser.spec.tsx +96 -0
- package/src/react/form/services/FormModel.ts +57 -39
- package/src/react/i18n/__tests__/I18nProvider.spec.ts +89 -0
- package/src/react/i18n/__tests__/locale-routing.spec.ts +107 -0
- package/src/react/i18n/providers/I18nProvider.ts +171 -12
- package/src/react/router/__tests__/RouterLocaleProvider.spec.ts +127 -0
- package/src/react/router/index.browser.ts +4 -0
- package/src/react/router/index.shared.ts +1 -0
- package/src/react/router/index.ts +9 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +15 -1
- package/src/react/router/providers/ReactPageProvider.ts +12 -1
- package/src/react/router/providers/ReactServerProvider.ts +92 -1
- package/src/react/router/providers/RootComponentsProvider.ts +13 -0
- package/src/react/router/providers/RouterLocaleProvider.ts +125 -0
- package/src/react/router/providers/__tests__/RootComponentsProvider.spec.ts +15 -0
- package/src/react/router/providers/__tests__/rootComponents.ssr.browser.spec.tsx +67 -0
- package/src/react/sitemap/__tests__/$sitemap.spec.ts +131 -0
- package/src/react/sitemap/index.browser.ts +21 -0
- package/src/react/sitemap/index.ts +25 -0
- package/src/react/sitemap/primitives/$sitemap.browser.ts +26 -0
- package/src/react/sitemap/primitives/$sitemap.ts +196 -0
- package/src/server/auth/__tests__/appleClientSecret.spec.ts +34 -0
- package/src/server/auth/__tests__/authFederationClient.spec.ts +40 -0
- package/src/server/auth/__tests__/federationAssertion.spec.ts +146 -0
- package/src/server/auth/__tests__/federationRedirectReplay.spec.ts +44 -0
- package/src/server/auth/helpers/appleClientSecret.ts +24 -0
- package/src/server/auth/helpers/federationAssertion.ts +74 -0
- package/src/server/auth/helpers/jtiReplayGuard.ts +41 -0
- package/src/server/auth/helpers/safeRedirectPath.ts +19 -0
- package/src/server/auth/index.ts +4 -0
- package/src/server/auth/primitives/$authFederationBroker.ts +273 -0
- package/src/server/auth/primitives/$authFederationClient.ts +89 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +18 -4
- package/src/server/cookies/__tests__/ServerCookiesProvider.spec.ts +70 -0
- package/src/server/cookies/providers/ServerCookiesProvider.ts +23 -3
- package/src/server/core/interfaces/ServerRequest.ts +8 -0
- package/src/server/core/primitives/$route.ts +27 -0
- package/src/cli/core/tasks/BuildSitemapTask.ts +0 -130
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/react/i18n/providers/I18nProvider.ts","../../../src/react/i18n/primitives/$dictionary.ts","../../../src/react/i18n/hooks/useI18n.ts","../../../src/react/i18n/components/Localize.tsx","../../../src/react/i18n/components/Translate.tsx","../../../src/react/i18n/index.ts"],"sourcesContent":["import { $hook, $inject, Alepha, TypeBoxError, TypeProvider, t } from \"alepha\";\nimport { type DateTime, DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $cookie } from \"alepha/server/cookies\";\nimport type { ServiceDictionary } from \"../hooks/useI18n.ts\";\n\nexport class I18nProvider<\n S extends object,\n K extends keyof ServiceDictionary<S>,\n> {\n protected log = $logger();\n protected alepha = $inject(Alepha);\n protected dateTimeProvider = $inject(DateTimeProvider);\n\n protected cookie = $cookie({\n name: \"lang\",\n schema: t.text(),\n ttl: [1, \"year\"],\n });\n\n public readonly registry: Array<{\n target: string;\n name: string;\n lang: string;\n loader: () => Promise<Record<string, string>>;\n translations: Record<string, string>;\n }> = [];\n\n options = {\n fallbackLang: \"en\",\n };\n\n public dateFormat: { format: (value: Date) => string } =\n new Intl.DateTimeFormat(this.lang);\n\n public numberFormat: { format: (value: number) => string } =\n new Intl.NumberFormat(this.lang);\n\n public get languages() {\n const languages = new Set<string>();\n\n for (const item of this.registry) {\n languages.add(item.lang);\n }\n\n return Array.from(languages);\n }\n\n constructor() {\n this.refreshLocale();\n }\n\n protected readonly onRender = $hook({\n on: \"server:onRequest\",\n priority: \"last\",\n handler: async ({ request }) => {\n this.alepha.store.set(\"alepha.react.i18n.lang\", this.cookie.get(request));\n },\n });\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n if (this.alepha.isBrowser()) {\n // get cookie lang\n const cookieLang = this.cookie.get();\n if (cookieLang) {\n this.alepha.store.set(\"alepha.react.i18n.lang\", cookieLang);\n }\n\n for (const item of this.registry) {\n if (item.lang === this.lang || item.lang === this.fallbackLang) {\n this.log.trace(\"Loading language\", {\n lang: item.lang,\n name: item.name,\n target: item.target,\n });\n item.translations = await item.loader();\n }\n }\n return;\n }\n\n for (const item of this.registry) {\n item.translations = await item.loader();\n }\n },\n });\n\n protected refreshLocale() {\n this.numberFormat = new Intl.NumberFormat(this.lang);\n this.dateFormat = new Intl.DateTimeFormat(this.lang);\n this.dateTimeProvider.setLocale(this.lang);\n TypeProvider.setLocale(this.lang);\n }\n\n public setLang = async (lang: string) => {\n if (this.alepha.isBrowser()) {\n for (const item of this.registry) {\n if (lang === item.lang) {\n if (Object.keys(item.translations).length > 0) {\n continue; // already loaded\n }\n item.translations = await item.loader();\n }\n }\n this.cookie.set(lang);\n }\n\n this.alepha.store.set(\"alepha.react.i18n.lang\", lang);\n this.refreshLocale();\n };\n\n protected readonly mutate = $hook({\n on: \"state:mutate\",\n handler: async ({ key, value }) => {\n if (key === \"alepha.react.i18n.lang\" && this.alepha.isBrowser()) {\n let hasChanged = false;\n for (const item of this.registry) {\n if (value === item.lang) {\n if (Object.keys(item.translations).length > 0) {\n continue; // already loaded\n }\n item.translations = await item.loader();\n hasChanged = true;\n }\n }\n\n this.refreshLocale();\n\n if (hasChanged) {\n this.alepha.store.set(\"alepha.react.i18n.lang\", value);\n }\n }\n },\n });\n\n public get fallbackLang(): string {\n const configured = this.options.fallbackLang;\n const hasDict = this.registry.some((item) => item.lang === configured);\n if (hasDict) {\n return configured;\n }\n return this.registry[0]?.lang ?? configured;\n }\n\n public get lang(): string {\n return this.alepha.store.get(\"alepha.react.i18n.lang\") || this.fallbackLang;\n }\n\n public translate = (key: string, args: string[] = []) => {\n for (const item of this.registry) {\n if (item.lang === this.lang) {\n if (item.translations[key]) {\n return this.render(item.translations[key], args); // append lang for fallback\n }\n }\n }\n\n for (const item of this.registry) {\n if (item.lang === this.fallbackLang) {\n if (item.translations[key]) {\n return this.render(item.translations[key], args); // append lang for fallback\n }\n }\n }\n\n return key; // fallback to the key itself if not found\n };\n\n public readonly l = (\n value: I18nLocalizeType,\n options: I18nLocalizeOptions = {},\n ) => {\n // Handle numbers\n if (typeof value === \"number\" && !options.date) {\n return new Intl.NumberFormat(this.lang, options.number).format(value);\n }\n\n // Handle dates\n if (\n value instanceof Date ||\n this.dateTimeProvider.isDateTime(value) ||\n (typeof value === \"string\" && options.date) ||\n (typeof value === \"number\" && options.date)\n ) {\n // convert to DateTime with locale applied\n let dt = this.dateTimeProvider.of(value);\n\n // apply timezone if specified\n if (options.timezone) {\n dt = dt.tz(options.timezone);\n }\n\n // format using dayjs format string\n if (typeof options.date === \"string\") {\n if (options.date === \"fromNow\") {\n return dt.locale(this.lang).fromNow();\n }\n return dt.locale(this.lang).format(options.date);\n }\n\n // format using Intl.DateTimeFormatOptions\n if (options.date) {\n return new Intl.DateTimeFormat(\n this.lang,\n options.timezone\n ? { ...options.date, timeZone: options.timezone }\n : options.date,\n ).format(dt.toDate());\n }\n\n // default formatting with timezone\n if (options.timezone) {\n return new Intl.DateTimeFormat(this.lang, {\n timeZone: options.timezone,\n }).format(dt.toDate());\n }\n\n // default formatting\n return new Intl.DateTimeFormat(this.lang).format(dt.toDate());\n }\n\n // handle TypeBox errors\n if (value instanceof TypeBoxError) {\n return TypeProvider.translateError(value, this.lang);\n }\n\n // return string values as-is\n return value;\n };\n\n /**\n * Look up `key` in the registered dictionaries. The `(string & {})` arm\n * keeps autocomplete for the typed dictionary keys while allowing shared\n * library components to pass arbitrary string keys (with a `default`\n * fallback) without casting to `as never`.\n */\n public readonly tr = (\n key: keyof ServiceDictionary<S>[K] | (string & {}),\n options: {\n args?: string[];\n default?: string;\n } = {},\n ) => {\n const translation = this.translate(key as string, options.args || []);\n if (translation === (key as string) && options.default) {\n return options.default;\n }\n return translation;\n };\n\n protected render(item: string, args: string[]): string {\n let result = item;\n for (let i = 0; i < args.length; i++) {\n result = result.replace(`$${i + 1}`, args[i]);\n }\n return result;\n }\n}\n\nexport type I18nLocalizeType = string | number | Date | DateTime | TypeBoxError;\n\nexport interface I18nLocalizeOptions {\n /**\n * Options for number formatting (when value is a number)\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat\n */\n number?: Intl.NumberFormatOptions;\n /**\n * Options for date formatting (when value is a Date or DateTime)\n * Can be:\n * - A dayjs format string (e.g., \"LLL\", \"YYYY-MM-DD\", \"dddd, MMMM D YYYY\")\n * - \"fromNow\" for relative time (e.g., \"2 hours ago\")\n * - Intl.DateTimeFormatOptions for native formatting\n * @see https://day.js.org/docs/en/display/format\n * @see https://day.js.org/docs/en/display/from-now\n */\n date?: string | \"fromNow\" | Intl.DateTimeFormatOptions;\n /**\n * Timezone to display dates in (when value is a Date or DateTime)\n * Uses IANA timezone names (e.g., \"America/New_York\", \"Europe/Paris\", \"Asia/Tokyo\")\n * @see https://day.js.org/docs/en/timezone/timezone\n */\n timezone?: string;\n}\n","import { $inject, type Async, createPrimitive, KIND, Primitive } from \"alepha\";\nimport { I18nProvider } from \"../providers/I18nProvider.ts\";\n\n/**\n * Register a dictionary entry for translations.\n *\n * It allows you to define a set of translations for a specific language.\n * Entry can be lazy-loaded, which is useful for large dictionaries or when translations are not needed immediately.\n *\n * @example\n * ```ts\n * import { $dictionary } from \"alepha/react/i18n\";\n *\n * const Example = () => {\n * const { tr } = useI18n<App, \"en\">();\n * return <div>{tr(\"hello\")}</div>; //\n * }\n *\n * class App {\n *\n * en = $dictionary({\n * // { default: { hello: \"Hey\" } }\n * lazy: () => import(\"./translations/en.ts\"),\n * });\n *\n * home = $page({\n * path: \"/\",\n * component: Example,\n * })\n * }\n *\n * run(App);\n * ```\n */\nexport const $dictionary = <T extends Record<string, string>>(\n options: DictionaryPrimitiveOptions<T>,\n): DictionaryPrimitive<T> => {\n return createPrimitive(DictionaryPrimitive<T>, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface DictionaryPrimitiveOptions<T extends Record<string, string>> {\n lang?: string;\n name?: string;\n lazy: () => Async<{ default: T }>;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class DictionaryPrimitive<\n T extends Record<string, string>,\n> extends Primitive<DictionaryPrimitiveOptions<T>> {\n protected provider = $inject(I18nProvider);\n protected onInit() {\n this.provider.registry.push({\n target: this.config.service.name,\n name: this.options.name ?? this.config.propertyKey,\n lang: this.options.lang ?? this.config.propertyKey,\n loader: async () => {\n const mod = await this.options.lazy();\n return mod.default;\n },\n translations: {},\n });\n }\n}\n\n$dictionary[KIND] = DictionaryPrimitive;\n","import { useInject, useStore } from \"alepha/react\";\nimport type { DictionaryPrimitive } from \"../primitives/$dictionary.ts\";\nimport { I18nProvider } from \"../providers/I18nProvider.ts\";\n\n/**\n * Hook to access the i18n service.\n */\nexport const useI18n = <\n S extends object,\n K extends keyof ServiceDictionary<S>,\n>(): I18nProvider<S, K> => {\n useStore(\"alepha.react.i18n.lang\");\n return useInject(I18nProvider<S, K>);\n};\n\nexport type ServiceDictionary<T extends object> = {\n [K in keyof T]: T[K] extends DictionaryPrimitive<infer U> ? U : never;\n};\n","import type { TypeBoxError } from \"alepha\";\nimport type { DateTime } from \"alepha/datetime\";\nimport { useI18n } from \"../hooks/useI18n.ts\";\n\nexport interface LocalizeProps {\n value: string | number | Date | DateTime | TypeBoxError;\n /**\n * Options for number formatting (when value is a number)\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat\n */\n number?: Intl.NumberFormatOptions;\n /**\n * Options for date formatting (when value is a Date or DateTime)\n * Can be:\n * - A dayjs format string (e.g., \"LLL\", \"YYYY-MM-DD\", \"dddd, MMMM D YYYY\")\n * - \"fromNow\" for relative time (e.g., \"2 hours ago\")\n * - Intl.DateTimeFormatOptions for native formatting\n * @see https://day.js.org/docs/en/display/format\n * @see https://day.js.org/docs/en/display/from-now\n */\n date?: string | \"fromNow\" | Intl.DateTimeFormatOptions;\n /**\n * Timezone to display dates in (when value is a Date or DateTime)\n * Uses IANA timezone names (e.g., \"America/New_York\", \"Europe/Paris\", \"Asia/Tokyo\")\n * @see https://day.js.org/docs/en/timezone/timezone\n */\n timezone?: string;\n}\n\nconst Localize = (props: LocalizeProps) => {\n const i18n = useI18n();\n return i18n.l(props.value, props);\n};\n\nexport default Localize;\n","import { useI18n } from \"../hooks/useI18n.ts\";\n\nexport interface TranslateProps {\n /**\n * Dictionary key to look up — the same key you would pass to `tr(...)`.\n */\n k: string;\n /**\n * Positional substitutions for `$1`, `$2`, … placeholders in the entry.\n */\n args?: string[];\n /**\n * Shown when the key is missing from every registered dictionary (otherwise\n * the raw key is rendered, matching `tr`'s behaviour).\n */\n fallback?: string;\n}\n\n/**\n * Renders a translated dictionary entry as a reactive text node.\n *\n * `tr(...)` is a hook, so it can only be called inside a component body. Use\n * `<Translate>` wherever a *node* is expected instead — a prop, a `children`\n * slot, or route metadata such as a nav-shell `label`. Because it subscribes to\n * {@link useI18n} it re-renders on a language switch, unlike a string resolved\n * once at module/initialisation time.\n *\n * Sibling of {@link Localize}: `Localize` formats a value (number, date,\n * error); `Translate` looks up a key.\n *\n * @example\n * ```tsx\n * nav: { label: <Translate k=\"admin.nav.users\" /> }\n * <Translate k=\"cart.items\" args={[String(count)]} fallback=\"Items\" />\n * ```\n */\nexport const Translate = (props: TranslateProps) => {\n const { tr } = useI18n();\n return <>{tr(props.k, { args: props.args, default: props.fallback })}</>;\n};\n\n/**\n * Terse alias for {@link Translate} — handy in dense JSX such as nav labels.\n */\nexport const Tr = Translate;\n\nexport default Translate;\n","import { $module } from \"alepha\";\nimport { $dictionary } from \"./primitives/$dictionary.ts\";\nimport { I18nProvider } from \"./providers/I18nProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type { LocalizeProps } from \"./components/Localize.tsx\";\nexport { default as Localize } from \"./components/Localize.tsx\";\nexport type { TranslateProps } from \"./components/Translate.tsx\";\nexport { default as Translate, Tr } from \"./components/Translate.tsx\";\nexport * from \"./hooks/useI18n.ts\";\nexport * from \"./primitives/$dictionary.ts\";\nexport * from \"./providers/I18nProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n export interface State {\n \"alepha.react.i18n.lang\"?: string;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Multi-language support.\n *\n * **Features:**\n * - Translation loading\n * - Locale detection\n * - Pluralization\n *\n * @module alepha.react.i18n\n */\nexport const AlephaReactI18n = $module({\n name: \"alepha.react.i18n\",\n primitives: [$dictionary],\n services: [I18nProvider],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/react/i18n/providers/I18nProvider.ts","../../../src/react/i18n/primitives/$dictionary.ts","../../../src/react/i18n/hooks/useI18n.ts","../../../src/react/i18n/components/Localize.tsx","../../../src/react/i18n/components/Translate.tsx","../../../src/react/i18n/index.ts"],"sourcesContent":["import { $hook, $inject, Alepha, TypeBoxError, TypeProvider, t } from \"alepha\";\nimport { type DateTime, DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\n// Locale-prefix routing is optional: the router module (one-directional\n// dependency, `i18n → router`) is only consulted when it is also registered.\nimport { RouterLocaleProvider } from \"alepha/react/router\";\nimport { $cookie } from \"alepha/server/cookies\";\nimport type { ServiceDictionary } from \"../hooks/useI18n.ts\";\n\nexport class I18nProvider<\n S extends object,\n K extends keyof ServiceDictionary<S>,\n> {\n protected log = $logger();\n protected alepha = $inject(Alepha);\n protected dateTimeProvider = $inject(DateTimeProvider);\n\n protected cookie = $cookie({\n name: \"lang\",\n schema: t.text(),\n ttl: [1, \"year\"],\n });\n\n public readonly registry: Array<{\n target: string;\n name: string;\n lang: string;\n loader: () => Promise<Record<string, string>>;\n translations: Record<string, string>;\n }> = [];\n\n options: {\n fallbackLang: string;\n autoDetect: boolean;\n routing: \"none\" | \"prefix\";\n } = {\n fallbackLang: \"en\",\n /**\n * When true (the default), the UI language for a first-time visitor (one\n * with no `lang` cookie) is detected server-side from the `Accept-Language`\n * header. A manually-selected language (the cookie) always takes\n * precedence, so this never overrides an explicit user choice. Set to false\n * to always start in `fallbackLang` regardless of the browser's preferred\n * language.\n */\n autoDetect: true,\n /**\n * URL strategy for languages:\n * - `\"none\"` (default): language lives in a cookie; URLs are not localized.\n * - `\"prefix\"`: each non-default language gets a path prefix (`/fr/about`),\n * making every language a distinct, crawlable URL for SEO. The default\n * language (`fallbackLang`) stays unprefixed. Requires the router module.\n * The URL becomes the source of truth for language (it wins over the\n * cookie / `Accept-Language`), and there is no automatic redirect.\n */\n routing: \"none\",\n };\n\n /**\n * Lazily-resolved locale-prefix router integration. Present only when both\n * the router module is registered AND `routing: \"prefix\"` was configured —\n * otherwise i18n stays fully standalone.\n */\n protected localeProviderResolved = false;\n protected localeProviderRef?: RouterLocaleProvider;\n protected get localeProvider(): RouterLocaleProvider | undefined {\n if (!this.localeProviderResolved) {\n this.localeProviderResolved = true;\n if (this.alepha.has(RouterLocaleProvider)) {\n this.localeProviderRef = this.alepha.inject(RouterLocaleProvider);\n }\n }\n return this.localeProviderRef;\n }\n\n public dateFormat: { format: (value: Date) => string } =\n new Intl.DateTimeFormat(this.lang);\n\n public numberFormat: { format: (value: number) => string } =\n new Intl.NumberFormat(this.lang);\n\n public get languages() {\n const languages = new Set<string>();\n\n for (const item of this.registry) {\n languages.add(item.lang);\n }\n\n return Array.from(languages);\n }\n\n constructor() {\n this.refreshLocale();\n }\n\n /**\n * Configure locale-prefix routing on the router, before the SSR routes are\n * registered (`priority: \"first\"` runs ahead of the router's own `configure`\n * hook). No-op unless `routing: \"prefix\"` and the router module is present.\n */\n protected readonly onConfigure = $hook({\n on: \"configure\",\n priority: \"first\",\n handler: () => {\n const localeProvider = this.localeProvider;\n if (this.options.routing === \"prefix\" && localeProvider) {\n localeProvider.configure({\n enabled: true,\n defaultLocale: this.fallbackLang,\n locales: this.languages,\n });\n }\n },\n });\n\n protected readonly onRender = $hook({\n on: \"server:onRequest\",\n priority: \"last\",\n handler: async ({ request }) => {\n this.alepha.store.set(\n \"alepha.react.i18n.lang\",\n this.resolveRequestLang(\n this.cookie.get(request),\n request.language,\n this.detectUrlLocale(request.url?.pathname),\n ),\n );\n },\n });\n\n /**\n * Detects the language carried by the request URL when `routing: \"prefix\"` is\n * active. Returns the locale for any URL (the prefixed one for `/fr/...`, the\n * default for an unprefixed path), or `undefined` when prefix routing is off.\n */\n protected detectUrlLocale(pathname: string | undefined): string | undefined {\n const localeProvider = this.localeProvider;\n if (localeProvider?.enabled && pathname) {\n return localeProvider.detect(pathname).locale || undefined;\n }\n return undefined;\n }\n\n /**\n * Resolves the UI language for an incoming server request.\n *\n * Priority:\n * 0. the URL locale prefix (`routing: \"prefix\"`) — the URL is the source of\n * truth and wins over everything, with no redirect;\n * 1. the `lang` cookie — a language the user manually selected;\n * 2. the `Accept-Language` header (when `autoDetect` is enabled) — but only\n * when the detected language is actually registered, so we never switch to\n * a locale we have no dictionary for. A region-qualified header (`en-US`)\n * matches an exact registration first, then its base language (`en`);\n * 3. `fallbackLang`.\n */\n protected resolveRequestLang(\n cookieLang: string | undefined,\n headerLang: string | undefined,\n urlLocale?: string,\n ): string {\n if (urlLocale) {\n return urlLocale;\n }\n\n if (cookieLang) {\n return cookieLang;\n }\n\n if (this.options.autoDetect && headerLang) {\n const registered = this.languages;\n for (const candidate of [headerLang, headerLang.split(\"-\")[0]]) {\n if (registered.includes(candidate)) {\n return candidate;\n }\n }\n }\n\n return this.fallbackLang;\n }\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: async () => {\n if (this.alepha.isBrowser()) {\n // In prefix mode the URL (hydrated into the lang state by the server)\n // is the source of truth, so the cookie must not override it.\n if (!this.localeProvider?.enabled) {\n const cookieLang = this.cookie.get();\n if (cookieLang) {\n this.alepha.store.set(\"alepha.react.i18n.lang\", cookieLang);\n }\n }\n\n for (const item of this.registry) {\n if (item.lang === this.lang || item.lang === this.fallbackLang) {\n this.log.trace(\"Loading language\", {\n lang: item.lang,\n name: item.name,\n target: item.target,\n });\n item.translations = await item.loader();\n }\n }\n return;\n }\n\n for (const item of this.registry) {\n item.translations = await item.loader();\n }\n },\n });\n\n protected refreshLocale() {\n this.numberFormat = new Intl.NumberFormat(this.lang);\n this.dateFormat = new Intl.DateTimeFormat(this.lang);\n this.dateTimeProvider.setLocale(this.lang);\n TypeProvider.setLocale(this.lang);\n }\n\n /**\n * Activates a language: lazily loads its dictionaries (browser), updates the\n * lang state, and refreshes the locale-bound formatters. Does NOT persist a\n * cookie or navigate — that is the caller's concern.\n */\n protected applyLang = async (lang: string) => {\n if (this.alepha.isBrowser()) {\n for (const item of this.registry) {\n if (lang === item.lang && Object.keys(item.translations).length === 0) {\n item.translations = await item.loader();\n }\n }\n }\n\n this.alepha.store.set(\"alepha.react.i18n.lang\", lang);\n this.refreshLocale();\n };\n\n public setLang = async (lang: string) => {\n const localeProvider = this.localeProvider;\n if (localeProvider?.enabled) {\n // The URL is the source of truth: switching language means navigating to\n // the same page under the new locale prefix. Dictionaries and formatters\n // are then activated via the resulting `alepha.react.router.locale`\n // change (see the `mutate` hook). No cookie is written in prefix mode.\n const { ReactRouter } = await import(\"alepha/react/router\");\n const router = this.alepha.inject(ReactRouter);\n const canonical = localeProvider.detect(router.pathname).pathname;\n await router.push(localeProvider.withPrefix(canonical, lang));\n return;\n }\n\n await this.applyLang(lang);\n if (this.alepha.isBrowser()) {\n this.cookie.set(lang);\n }\n };\n\n protected readonly mutate = $hook({\n on: \"state:mutate\",\n handler: async ({ key, value }) => {\n // Prefix-mode navigation changed the active locale (set by the router) →\n // activate the matching language (loads dictionaries, refreshes\n // formatters, and re-renders consumers via the lang state).\n if (\n key === \"alepha.react.router.locale\" &&\n this.localeProvider?.enabled\n ) {\n const lang = (value as string) || this.fallbackLang;\n if (lang !== this.lang) {\n await this.applyLang(lang);\n }\n return;\n }\n\n if (key === \"alepha.react.i18n.lang\" && this.alepha.isBrowser()) {\n let hasChanged = false;\n for (const item of this.registry) {\n if (value === item.lang) {\n if (Object.keys(item.translations).length > 0) {\n continue; // already loaded\n }\n item.translations = await item.loader();\n hasChanged = true;\n }\n }\n\n this.refreshLocale();\n\n if (hasChanged) {\n this.alepha.store.set(\"alepha.react.i18n.lang\", value);\n }\n }\n },\n });\n\n public get fallbackLang(): string {\n const configured = this.options.fallbackLang;\n const hasDict = this.registry.some((item) => item.lang === configured);\n if (hasDict) {\n return configured;\n }\n return this.registry[0]?.lang ?? configured;\n }\n\n public get lang(): string {\n return this.alepha.store.get(\"alepha.react.i18n.lang\") || this.fallbackLang;\n }\n\n public translate = (key: string, args: string[] = []) => {\n for (const item of this.registry) {\n if (item.lang === this.lang) {\n if (item.translations[key]) {\n return this.render(item.translations[key], args); // append lang for fallback\n }\n }\n }\n\n for (const item of this.registry) {\n if (item.lang === this.fallbackLang) {\n if (item.translations[key]) {\n return this.render(item.translations[key], args); // append lang for fallback\n }\n }\n }\n\n return key; // fallback to the key itself if not found\n };\n\n public readonly l = (\n value: I18nLocalizeType,\n options: I18nLocalizeOptions = {},\n ) => {\n // Handle numbers\n if (typeof value === \"number\" && !options.date) {\n return new Intl.NumberFormat(this.lang, options.number).format(value);\n }\n\n // Handle dates\n if (\n value instanceof Date ||\n this.dateTimeProvider.isDateTime(value) ||\n (typeof value === \"string\" && options.date) ||\n (typeof value === \"number\" && options.date)\n ) {\n // convert to DateTime with locale applied\n let dt = this.dateTimeProvider.of(value);\n\n // apply timezone if specified\n if (options.timezone) {\n dt = dt.tz(options.timezone);\n }\n\n // format using dayjs format string\n if (typeof options.date === \"string\") {\n if (options.date === \"fromNow\") {\n return dt.locale(this.lang).fromNow();\n }\n return dt.locale(this.lang).format(options.date);\n }\n\n // format using Intl.DateTimeFormatOptions\n if (options.date) {\n return new Intl.DateTimeFormat(\n this.lang,\n options.timezone\n ? { ...options.date, timeZone: options.timezone }\n : options.date,\n ).format(dt.toDate());\n }\n\n // default formatting with timezone\n if (options.timezone) {\n return new Intl.DateTimeFormat(this.lang, {\n timeZone: options.timezone,\n }).format(dt.toDate());\n }\n\n // default formatting\n return new Intl.DateTimeFormat(this.lang).format(dt.toDate());\n }\n\n // handle TypeBox errors\n if (value instanceof TypeBoxError) {\n return TypeProvider.translateError(value, this.lang);\n }\n\n // return string values as-is\n return value;\n };\n\n /**\n * Look up `key` in the registered dictionaries. The `(string & {})` arm\n * keeps autocomplete for the typed dictionary keys while allowing shared\n * library components to pass arbitrary string keys (with a `default`\n * fallback) without casting to `as never`.\n */\n public readonly tr = (\n key: keyof ServiceDictionary<S>[K] | (string & {}),\n options: {\n args?: string[];\n default?: string;\n } = {},\n ) => {\n const translation = this.translate(key as string, options.args || []);\n if (translation === (key as string) && options.default) {\n return options.default;\n }\n return translation;\n };\n\n protected render(item: string, args: string[]): string {\n let result = item;\n for (let i = 0; i < args.length; i++) {\n result = result.replace(`$${i + 1}`, args[i]);\n }\n return result;\n }\n}\n\nexport type I18nLocalizeType = string | number | Date | DateTime | TypeBoxError;\n\nexport interface I18nLocalizeOptions {\n /**\n * Options for number formatting (when value is a number)\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat\n */\n number?: Intl.NumberFormatOptions;\n /**\n * Options for date formatting (when value is a Date or DateTime)\n * Can be:\n * - A dayjs format string (e.g., \"LLL\", \"YYYY-MM-DD\", \"dddd, MMMM D YYYY\")\n * - \"fromNow\" for relative time (e.g., \"2 hours ago\")\n * - Intl.DateTimeFormatOptions for native formatting\n * @see https://day.js.org/docs/en/display/format\n * @see https://day.js.org/docs/en/display/from-now\n */\n date?: string | \"fromNow\" | Intl.DateTimeFormatOptions;\n /**\n * Timezone to display dates in (when value is a Date or DateTime)\n * Uses IANA timezone names (e.g., \"America/New_York\", \"Europe/Paris\", \"Asia/Tokyo\")\n * @see https://day.js.org/docs/en/timezone/timezone\n */\n timezone?: string;\n}\n","import { $inject, type Async, createPrimitive, KIND, Primitive } from \"alepha\";\nimport { I18nProvider } from \"../providers/I18nProvider.ts\";\n\n/**\n * Register a dictionary entry for translations.\n *\n * It allows you to define a set of translations for a specific language.\n * Entry can be lazy-loaded, which is useful for large dictionaries or when translations are not needed immediately.\n *\n * @example\n * ```ts\n * import { $dictionary } from \"alepha/react/i18n\";\n *\n * const Example = () => {\n * const { tr } = useI18n<App, \"en\">();\n * return <div>{tr(\"hello\")}</div>; //\n * }\n *\n * class App {\n *\n * en = $dictionary({\n * // { default: { hello: \"Hey\" } }\n * lazy: () => import(\"./translations/en.ts\"),\n * });\n *\n * home = $page({\n * path: \"/\",\n * component: Example,\n * })\n * }\n *\n * run(App);\n * ```\n */\nexport const $dictionary = <T extends Record<string, string>>(\n options: DictionaryPrimitiveOptions<T>,\n): DictionaryPrimitive<T> => {\n return createPrimitive(DictionaryPrimitive<T>, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface DictionaryPrimitiveOptions<T extends Record<string, string>> {\n lang?: string;\n name?: string;\n lazy: () => Async<{ default: T }>;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class DictionaryPrimitive<\n T extends Record<string, string>,\n> extends Primitive<DictionaryPrimitiveOptions<T>> {\n protected provider = $inject(I18nProvider);\n protected onInit() {\n this.provider.registry.push({\n target: this.config.service.name,\n name: this.options.name ?? this.config.propertyKey,\n lang: this.options.lang ?? this.config.propertyKey,\n loader: async () => {\n const mod = await this.options.lazy();\n return mod.default;\n },\n translations: {},\n });\n }\n}\n\n$dictionary[KIND] = DictionaryPrimitive;\n","import { useInject, useStore } from \"alepha/react\";\nimport type { DictionaryPrimitive } from \"../primitives/$dictionary.ts\";\nimport { I18nProvider } from \"../providers/I18nProvider.ts\";\n\n/**\n * Hook to access the i18n service.\n */\nexport const useI18n = <\n S extends object,\n K extends keyof ServiceDictionary<S>,\n>(): I18nProvider<S, K> => {\n useStore(\"alepha.react.i18n.lang\");\n return useInject(I18nProvider<S, K>);\n};\n\nexport type ServiceDictionary<T extends object> = {\n [K in keyof T]: T[K] extends DictionaryPrimitive<infer U> ? U : never;\n};\n","import type { TypeBoxError } from \"alepha\";\nimport type { DateTime } from \"alepha/datetime\";\nimport { useI18n } from \"../hooks/useI18n.ts\";\n\nexport interface LocalizeProps {\n value: string | number | Date | DateTime | TypeBoxError;\n /**\n * Options for number formatting (when value is a number)\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat\n */\n number?: Intl.NumberFormatOptions;\n /**\n * Options for date formatting (when value is a Date or DateTime)\n * Can be:\n * - A dayjs format string (e.g., \"LLL\", \"YYYY-MM-DD\", \"dddd, MMMM D YYYY\")\n * - \"fromNow\" for relative time (e.g., \"2 hours ago\")\n * - Intl.DateTimeFormatOptions for native formatting\n * @see https://day.js.org/docs/en/display/format\n * @see https://day.js.org/docs/en/display/from-now\n */\n date?: string | \"fromNow\" | Intl.DateTimeFormatOptions;\n /**\n * Timezone to display dates in (when value is a Date or DateTime)\n * Uses IANA timezone names (e.g., \"America/New_York\", \"Europe/Paris\", \"Asia/Tokyo\")\n * @see https://day.js.org/docs/en/timezone/timezone\n */\n timezone?: string;\n}\n\nconst Localize = (props: LocalizeProps) => {\n const i18n = useI18n();\n return i18n.l(props.value, props);\n};\n\nexport default Localize;\n","import { useI18n } from \"../hooks/useI18n.ts\";\n\nexport interface TranslateProps {\n /**\n * Dictionary key to look up — the same key you would pass to `tr(...)`.\n */\n k: string;\n /**\n * Positional substitutions for `$1`, `$2`, … placeholders in the entry.\n */\n args?: string[];\n /**\n * Shown when the key is missing from every registered dictionary (otherwise\n * the raw key is rendered, matching `tr`'s behaviour).\n */\n fallback?: string;\n}\n\n/**\n * Renders a translated dictionary entry as a reactive text node.\n *\n * `tr(...)` is a hook, so it can only be called inside a component body. Use\n * `<Translate>` wherever a *node* is expected instead — a prop, a `children`\n * slot, or route metadata such as a nav-shell `label`. Because it subscribes to\n * {@link useI18n} it re-renders on a language switch, unlike a string resolved\n * once at module/initialisation time.\n *\n * Sibling of {@link Localize}: `Localize` formats a value (number, date,\n * error); `Translate` looks up a key.\n *\n * @example\n * ```tsx\n * nav: { label: <Translate k=\"admin.nav.users\" /> }\n * <Translate k=\"cart.items\" args={[String(count)]} fallback=\"Items\" />\n * ```\n */\nexport const Translate = (props: TranslateProps) => {\n const { tr } = useI18n();\n return <>{tr(props.k, { args: props.args, default: props.fallback })}</>;\n};\n\n/**\n * Terse alias for {@link Translate} — handy in dense JSX such as nav labels.\n */\nexport const Tr = Translate;\n\nexport default Translate;\n","import { $module } from \"alepha\";\nimport { $dictionary } from \"./primitives/$dictionary.ts\";\nimport { I18nProvider } from \"./providers/I18nProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type { LocalizeProps } from \"./components/Localize.tsx\";\nexport { default as Localize } from \"./components/Localize.tsx\";\nexport type { TranslateProps } from \"./components/Translate.tsx\";\nexport { default as Translate, Tr } from \"./components/Translate.tsx\";\nexport * from \"./hooks/useI18n.ts\";\nexport * from \"./primitives/$dictionary.ts\";\nexport * from \"./providers/I18nProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n export interface State {\n \"alepha.react.i18n.lang\"?: string;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Multi-language support.\n *\n * **Features:**\n * - Translation loading\n * - Locale detection\n * - Pluralization\n *\n * @module alepha.react.i18n\n */\nexport const AlephaReactI18n = $module({\n name: \"alepha.react.i18n\",\n primitives: [$dictionary],\n services: [I18nProvider],\n});\n"],"mappings":"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"}
|
|
@@ -719,6 +719,117 @@ function parseAnimation(animationLike, state, type = "enter") {
|
|
|
719
719
|
}
|
|
720
720
|
}
|
|
721
721
|
//#endregion
|
|
722
|
+
//#region ../../src/react/router/providers/RootComponentsProvider.ts
|
|
723
|
+
/**
|
|
724
|
+
* Extension point letting any module contribute root-level React nodes that
|
|
725
|
+
* render on every page (siblings of the page view, inside AlephaContext).
|
|
726
|
+
*
|
|
727
|
+
* A module pushes into `rootComponents` from its `register` hook; the array
|
|
728
|
+
* is rendered by `ReactPageProvider.root()`. SSR-safe (same element feeds
|
|
729
|
+
* server render + client hydrate).
|
|
730
|
+
*/
|
|
731
|
+
var RootComponentsProvider = class {
|
|
732
|
+
rootComponents = [];
|
|
733
|
+
};
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region ../../src/react/router/providers/RouterLocaleProvider.ts
|
|
736
|
+
/**
|
|
737
|
+
* Generic locale path-prefix mechanism for the router.
|
|
738
|
+
*
|
|
739
|
+
* This provider knows nothing about i18n — it only deals with URL path
|
|
740
|
+
* segments. It is configured by the i18n module (`I18nProvider`) when
|
|
741
|
+
* `routing: "prefix"` is enabled, which keeps the dependency one-directional
|
|
742
|
+
* (`i18n → router`) and avoids a module cycle.
|
|
743
|
+
*
|
|
744
|
+
* The default locale is served WITHOUT a prefix (`/about` = default,
|
|
745
|
+
* `/fr/about` = French). The active locale is derived from the current
|
|
746
|
+
* request/navigation and stored under `alepha.react.router.locale`, so every
|
|
747
|
+
* URL the router builds (`pathname()`) automatically carries the right prefix.
|
|
748
|
+
*/
|
|
749
|
+
var RouterLocaleProvider = class {
|
|
750
|
+
alepha = $inject(Alepha);
|
|
751
|
+
/**
|
|
752
|
+
* Whether locale path-prefixing is active. Off by default — opt-in via the
|
|
753
|
+
* i18n module.
|
|
754
|
+
*/
|
|
755
|
+
enabled = false;
|
|
756
|
+
/**
|
|
757
|
+
* The default locale, served without a path prefix (e.g. `"en"` → `/about`).
|
|
758
|
+
*/
|
|
759
|
+
defaultLocale = "";
|
|
760
|
+
/**
|
|
761
|
+
* All known locales, including the default one.
|
|
762
|
+
*/
|
|
763
|
+
locales = [];
|
|
764
|
+
/**
|
|
765
|
+
* Configure the provider. Called by the i18n module before the SSR routes
|
|
766
|
+
* are registered.
|
|
767
|
+
*/
|
|
768
|
+
configure(options) {
|
|
769
|
+
if (options.enabled !== void 0) this.enabled = options.enabled;
|
|
770
|
+
if (options.defaultLocale !== void 0) this.defaultLocale = options.defaultLocale;
|
|
771
|
+
if (options.locales !== void 0) this.locales = options.locales;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Locales that carry a URL prefix — every known locale except the default.
|
|
775
|
+
*/
|
|
776
|
+
get prefixedLocales() {
|
|
777
|
+
return this.locales.filter((locale) => locale !== this.defaultLocale);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Splits a leading locale segment off a pathname.
|
|
781
|
+
*
|
|
782
|
+
* - `/fr/about` → `{ locale: "fr", pathname: "/about" }` when `fr` is a
|
|
783
|
+
* prefixed locale.
|
|
784
|
+
* - `/about` → `{ locale: defaultLocale, pathname: "/about" }`.
|
|
785
|
+
*
|
|
786
|
+
* When prefixing is disabled the pathname is returned untouched.
|
|
787
|
+
*/
|
|
788
|
+
detect(pathname) {
|
|
789
|
+
if (this.enabled) {
|
|
790
|
+
const first = pathname.split("/")[1];
|
|
791
|
+
if (first && this.prefixedLocales.includes(first)) {
|
|
792
|
+
const rest = pathname.slice(first.length + 1);
|
|
793
|
+
return {
|
|
794
|
+
locale: first,
|
|
795
|
+
pathname: this.normalize(rest)
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
locale: this.defaultLocale,
|
|
801
|
+
pathname
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Prepends the locale prefix to a pathname when needed. The default locale
|
|
806
|
+
* (and any unknown/disabled case) returns the pathname unchanged.
|
|
807
|
+
*/
|
|
808
|
+
withPrefix(pathname, locale = this.current) {
|
|
809
|
+
if (!this.enabled || !locale || locale === this.defaultLocale || !this.prefixedLocales.includes(locale)) return pathname;
|
|
810
|
+
return `/${locale}${pathname === "/" ? "" : pathname}`;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* The active locale, derived from the current request/navigation. Falls back
|
|
814
|
+
* to the default locale when nothing has been detected.
|
|
815
|
+
*/
|
|
816
|
+
get current() {
|
|
817
|
+
return this.alepha.store.get("alepha.react.router.locale") || this.defaultLocale;
|
|
818
|
+
}
|
|
819
|
+
set current(locale) {
|
|
820
|
+
this.alepha.store.set("alepha.react.router.locale", locale);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Normalizes a stripped pathname so it always starts with a single slash and
|
|
824
|
+
* carries no trailing slash (except the root `/`).
|
|
825
|
+
*/
|
|
826
|
+
normalize(pathname) {
|
|
827
|
+
if (!pathname || pathname === "/") return "/";
|
|
828
|
+
const withLeading = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
829
|
+
return withLeading.length > 1 && withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
//#endregion
|
|
722
833
|
//#region ../../src/react/router/providers/ReactPageProvider.ts
|
|
723
834
|
const reactPageOptions = $atom({
|
|
724
835
|
name: "alepha.react.page.options",
|
|
@@ -752,6 +863,8 @@ var ReactPageProvider = class {
|
|
|
752
863
|
log = $logger();
|
|
753
864
|
options = $state(reactPageOptions);
|
|
754
865
|
alepha = $inject(Alepha);
|
|
866
|
+
rootComponentsProvider = $inject(RootComponentsProvider);
|
|
867
|
+
localeProvider = $inject(RouterLocaleProvider);
|
|
755
868
|
pages = [];
|
|
756
869
|
nextIdCursor = 0;
|
|
757
870
|
configure = $hook({
|
|
@@ -834,17 +947,19 @@ var ReactPageProvider = class {
|
|
|
834
947
|
parent = parent.parent;
|
|
835
948
|
}
|
|
836
949
|
url = this.compile(url, options.params ?? {});
|
|
950
|
+
url = url.replace(/\/\/+/g, "/") || "/";
|
|
951
|
+
url = this.localeProvider.withPrefix(url);
|
|
837
952
|
if (options.query) {
|
|
838
953
|
const query = new URLSearchParams(options.query);
|
|
839
954
|
if (query.toString()) url += `?${query.toString()}`;
|
|
840
955
|
}
|
|
841
|
-
return url
|
|
956
|
+
return url;
|
|
842
957
|
}
|
|
843
958
|
url(name, options = {}) {
|
|
844
959
|
return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
|
|
845
960
|
}
|
|
846
961
|
root(state) {
|
|
847
|
-
const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element));
|
|
962
|
+
const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element), ...this.rootComponentsProvider.rootComponents);
|
|
848
963
|
if (this.options.strictMode) return createElement(StrictMode, {}, root);
|
|
849
964
|
return root;
|
|
850
965
|
}
|
|
@@ -1162,6 +1277,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
1162
1277
|
alepha = $inject(Alepha);
|
|
1163
1278
|
pageApi = $inject(ReactPageProvider);
|
|
1164
1279
|
browserHeadProvider = $inject(BrowserHeadProvider);
|
|
1280
|
+
localeProvider = $inject(RouterLocaleProvider);
|
|
1165
1281
|
add(entry) {
|
|
1166
1282
|
this.pageApi.add(entry);
|
|
1167
1283
|
}
|
|
@@ -1190,7 +1306,13 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
1190
1306
|
state
|
|
1191
1307
|
});
|
|
1192
1308
|
try {
|
|
1193
|
-
|
|
1309
|
+
let matchPathname = pathname;
|
|
1310
|
+
if (this.localeProvider.enabled) {
|
|
1311
|
+
const detected = this.localeProvider.detect(pathname);
|
|
1312
|
+
this.localeProvider.current = detected.locale;
|
|
1313
|
+
matchPathname = detected.pathname;
|
|
1314
|
+
}
|
|
1315
|
+
const { route, params } = this.match(matchPathname);
|
|
1194
1316
|
const query = {};
|
|
1195
1317
|
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
1196
1318
|
state.name = route?.page.name;
|
|
@@ -1822,11 +1944,12 @@ const AlephaReactRouter = $module({
|
|
|
1822
1944
|
ReactBrowserProvider,
|
|
1823
1945
|
ReactRouter,
|
|
1824
1946
|
ReactBrowserRendererProvider,
|
|
1947
|
+
RouterLocaleProvider,
|
|
1825
1948
|
ReactPageService
|
|
1826
1949
|
],
|
|
1827
|
-
register: (alepha) => alepha.with(AlephaReact).with(AlephaReactHead).with(AlephaDateTime).with(AlephaServer).with(AlephaServerLinks).with(ReactPageProvider).with(ReactBrowserProvider).with(ReactBrowserRouterProvider).with(ReactBrowserRendererProvider).with(ReactRouter)
|
|
1950
|
+
register: (alepha) => alepha.with(AlephaReact).with(AlephaReactHead).with(AlephaDateTime).with(AlephaServer).with(AlephaServerLinks).with(ReactPageProvider).with(ReactBrowserProvider).with(ReactBrowserRouterProvider).with(ReactBrowserRendererProvider).with(RouterLocaleProvider).with(ReactRouter)
|
|
1828
1951
|
});
|
|
1829
1952
|
//#endregion
|
|
1830
|
-
export { $page, AlephaReactRouter, ErrorViewer, Link, NestedView_default as NestedView, NotFound, PAGE_PRELOAD_KEY, PagePrimitive, ReactBrowserProvider, ReactBrowserRendererProvider, ReactBrowserRouterProvider, ReactPageProvider, ReactPageService, ReactRouter, Redirection, RouterLayerContext, isPageRoute, reactBrowserOptions, reactPageOptions, useActive, useQueryParams, useRouter, useRouterState };
|
|
1953
|
+
export { $page, AlephaReactRouter, ErrorViewer, Link, NestedView_default as NestedView, NotFound, PAGE_PRELOAD_KEY, PagePrimitive, ReactBrowserProvider, ReactBrowserRendererProvider, ReactBrowserRouterProvider, ReactPageProvider, ReactPageService, ReactRouter, Redirection, RootComponentsProvider, RouterLayerContext, RouterLocaleProvider, isPageRoute, reactBrowserOptions, reactPageOptions, useActive, useQueryParams, useRouter, useRouterState };
|
|
1831
1954
|
|
|
1832
1955
|
//# sourceMappingURL=index.browser.js.map
|