@byline/i18n 2.6.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.
Files changed (44) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +166 -0
  3. package/dist/admin/en.js +424 -0
  4. package/dist/admin/fr.js +424 -0
  5. package/dist/admin/index.d.ts +44 -0
  6. package/dist/admin/index.js +27 -0
  7. package/dist/formatter.d.ts +40 -0
  8. package/dist/formatter.js +65 -0
  9. package/dist/index.d.ts +33 -0
  10. package/dist/index.js +3 -0
  11. package/dist/merge.d.ts +39 -0
  12. package/dist/merge.js +58 -0
  13. package/dist/react/i18n-context.d.ts +29 -0
  14. package/dist/react/i18n-context.js +3 -0
  15. package/dist/react/i18n-provider.d.ts +31 -0
  16. package/dist/react/i18n-provider.js +38 -0
  17. package/dist/react/index.d.ts +24 -0
  18. package/dist/react/index.js +4 -0
  19. package/dist/react/language-menu.d.ts +15 -0
  20. package/dist/react/language-menu.js +78 -0
  21. package/dist/react/use-translation.d.ts +24 -0
  22. package/dist/react/use-translation.js +16 -0
  23. package/dist/resolve.d.ts +21 -0
  24. package/dist/resolve.js +28 -0
  25. package/dist/types.d.ts +65 -0
  26. package/dist/types.js +1 -0
  27. package/package.json +100 -0
  28. package/src/admin/en.json +423 -0
  29. package/src/admin/fr.json +423 -0
  30. package/src/admin/index.test.node.ts +68 -0
  31. package/src/admin/index.ts +99 -0
  32. package/src/formatter.test.node.ts +163 -0
  33. package/src/formatter.ts +166 -0
  34. package/src/index.ts +47 -0
  35. package/src/merge.test.node.ts +85 -0
  36. package/src/merge.ts +135 -0
  37. package/src/react/i18n-context.ts +45 -0
  38. package/src/react/i18n-provider.tsx +76 -0
  39. package/src/react/index.ts +26 -0
  40. package/src/react/language-menu.tsx +115 -0
  41. package/src/react/use-translation.ts +45 -0
  42. package/src/resolve.test.node.ts +128 -0
  43. package/src/resolve.ts +84 -0
  44. package/src/types.ts +72 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import enJson from './en.json';
9
+ import type { LocaleCode, NamespaceTranslations, TranslationBundle } from '../types.js';
10
+ declare const en: NamespaceTranslations;
11
+ declare const fr: NamespaceTranslations;
12
+ /** Locale codes for which a bundled translation ships in-package. */
13
+ export declare const bundledLocales: readonly LocaleCode[];
14
+ /** Re-exported as typed consts for plugin authors who want the literal-key autocomplete. */
15
+ export { en, fr };
16
+ export type AdminNamespaceTranslations = typeof enJson;
17
+ export interface AdminTranslationsOptions {
18
+ /**
19
+ * Locale codes to include in the returned bundle. Each must appear in
20
+ * `bundledLocales` — unknown codes throw at config time. Defaults to
21
+ * `['en']` when omitted, which is always available.
22
+ */
23
+ locales?: readonly LocaleCode[];
24
+ }
25
+ /**
26
+ * Build a `TranslationBundle` carrying the `byline-admin` namespace for
27
+ * each requested locale. Compose with plugin / extension bundles via
28
+ * `mergeTranslations(...)` in the host's `admin.config.ts`.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { adminTranslations } from '@byline/i18n/admin'
33
+ *
34
+ * defineClientConfig({
35
+ * i18n: {
36
+ * interface: { defaultLocale: 'en', locales: ['en', 'fr'] },
37
+ * translations: adminTranslations({ locales: ['en', 'fr'] }),
38
+ * },
39
+ * })
40
+ * ```
41
+ *
42
+ * @throws when a requested code is not in `bundledLocales`.
43
+ */
44
+ export declare function adminTranslations(options?: AdminTranslationsOptions): TranslationBundle;
@@ -0,0 +1,27 @@
1
+ import { mergeTranslations } from "../merge.js";
2
+ import en from "./en.js";
3
+ import fr from "./fr.js";
4
+ const admin_en = en;
5
+ const admin_fr = fr;
6
+ const BUNDLES = {
7
+ en: admin_en,
8
+ fr: admin_fr
9
+ };
10
+ const bundledLocales = Object.freeze(Object.keys(BUNDLES));
11
+ function adminTranslations(options = {}) {
12
+ const locales = options.locales ?? [
13
+ 'en'
14
+ ];
15
+ const partials = [];
16
+ for (const locale of locales){
17
+ const bundle = BUNDLES[locale];
18
+ if (null == bundle) throw new Error(`[adminTranslations] no bundled translation for locale '${locale}'. Available: [${bundledLocales.join(', ')}]. To add a locale, drop a new JSON file in @byline/i18n/src/admin/.`);
19
+ partials.push({
20
+ [locale]: {
21
+ 'byline-admin': bundle
22
+ }
23
+ });
24
+ }
25
+ return mergeTranslations(...partials);
26
+ }
27
+ export { adminTranslations, admin_en as en, admin_fr as fr, bundledLocales };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { LocaleCode, MessageKey, Namespace, TranslationBundle, TranslationValues } from './types.js';
9
+ export interface FormatterOptions {
10
+ bundle: TranslationBundle;
11
+ /** The locale to look up first. */
12
+ activeLocale: LocaleCode;
13
+ /** Last-resort lookup before falling back to the raw key. */
14
+ defaultLocale: LocaleCode;
15
+ /**
16
+ * Invoked once per `(locale, namespace, key)` triple when step 1
17
+ * misses but step 2 hits. Used by the React provider to emit
18
+ * `console.warn` lines in development. Pass `undefined` to suppress.
19
+ */
20
+ onMissing?: (event: MissingTranslationEvent) => void;
21
+ }
22
+ export interface MissingTranslationEvent {
23
+ activeLocale: LocaleCode;
24
+ namespace: Namespace;
25
+ key: MessageKey;
26
+ /** Whether the default-locale lookup also missed. */
27
+ fellThroughToKey: boolean;
28
+ }
29
+ export interface Formatter {
30
+ /**
31
+ * Format a message for `namespace.key` with `values`. Always returns
32
+ * a string. ICU error paths (malformed message, missing required
33
+ * argument) fall through to the raw key rather than throwing — the
34
+ * admin shell should never crash because of a translation bug.
35
+ */
36
+ t(namespace: Namespace, key: MessageKey, values?: TranslationValues): string;
37
+ readonly activeLocale: LocaleCode;
38
+ readonly defaultLocale: LocaleCode;
39
+ }
40
+ export declare function createFormatter(options: FormatterOptions): Formatter;
@@ -0,0 +1,65 @@
1
+ import { IntlMessageFormat } from "intl-messageformat";
2
+ function createFormatter(options) {
3
+ const { bundle, activeLocale, defaultLocale, onMissing } = options;
4
+ const formatterCache = new Map();
5
+ const missingReported = new Set();
6
+ function lookupMessage(locale, namespace, key) {
7
+ return bundle[locale]?.[namespace]?.[key];
8
+ }
9
+ function getFormatter(locale, namespace, key) {
10
+ const cacheKey = `${locale}\0${namespace}\0${key}`;
11
+ const cached = formatterCache.get(cacheKey);
12
+ if (void 0 !== cached) return cached;
13
+ const message = lookupMessage(locale, namespace, key);
14
+ if (null == message) {
15
+ formatterCache.set(cacheKey, null);
16
+ return null;
17
+ }
18
+ try {
19
+ const formatter = new IntlMessageFormat(message, locale);
20
+ formatterCache.set(cacheKey, formatter);
21
+ return formatter;
22
+ } catch {
23
+ formatterCache.set(cacheKey, null);
24
+ return null;
25
+ }
26
+ }
27
+ function reportMissing(namespace, key, fellThroughToKey) {
28
+ if (null == onMissing) return;
29
+ const reportKey = `${activeLocale}\0${namespace}\0${key}\0${fellThroughToKey ? '1' : '0'}`;
30
+ if (missingReported.has(reportKey)) return;
31
+ missingReported.add(reportKey);
32
+ onMissing({
33
+ activeLocale,
34
+ namespace,
35
+ key,
36
+ fellThroughToKey
37
+ });
38
+ }
39
+ return {
40
+ activeLocale,
41
+ defaultLocale,
42
+ t (namespace, key, values) {
43
+ const activeFormatter = getFormatter(activeLocale, namespace, key);
44
+ if (null != activeFormatter) return formatSafe(activeFormatter, values, key);
45
+ if (defaultLocale !== activeLocale) {
46
+ const defaultFormatter = getFormatter(defaultLocale, namespace, key);
47
+ if (null != defaultFormatter) {
48
+ reportMissing(namespace, key, false);
49
+ return formatSafe(defaultFormatter, values, key);
50
+ }
51
+ }
52
+ reportMissing(namespace, key, true);
53
+ return key;
54
+ }
55
+ };
56
+ }
57
+ function formatSafe(formatter, values, fallbackKey) {
58
+ try {
59
+ const out = formatter.format(values);
60
+ return 'string' == typeof out ? out : fallbackKey;
61
+ } catch {
62
+ return fallbackKey;
63
+ }
64
+ }
65
+ export { createFormatter };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * `@byline/i18n` — admin interface translation primitives.
10
+ *
11
+ * Root entry: pure, React-free, safe to import from server contexts
12
+ * (loaders, server fns, Workers). Carries the type definitions, the
13
+ * `mergeTranslations` registry helper, the ICU formatter, and the
14
+ * locale-resolution cascade.
15
+ *
16
+ * The React surface (`<I18nProvider>`, `useTranslation`,
17
+ * `<LanguageMenu>`) lives at `@byline/i18n/react` — single barrel,
18
+ * single React Context identity, to sidestep the Vite `optimizeDeps`
19
+ * trap that has bitten this codebase before (see `@byline/ui`'s
20
+ * `react.ts` comment).
21
+ *
22
+ * Admin bundles (`adminTranslations(...)`, the English bundle) live
23
+ * at `@byline/i18n/admin`.
24
+ *
25
+ * See `docs/I18N.md` for the architecture.
26
+ */
27
+ export { createFormatter } from './formatter.js';
28
+ export { mergeTranslations } from './merge.js';
29
+ export { resolveInterfaceLocale } from './resolve.js';
30
+ export type { Formatter, FormatterOptions, MissingTranslationEvent, } from './formatter.js';
31
+ export type { MergeOptions, TranslationCollision } from './merge.js';
32
+ export type { ResolveInterfaceLocaleOptions } from './resolve.js';
33
+ export type { LocaleCode, LocaleDefinition, MessageKey, Namespace, NamespaceTranslations, TranslationBundle, TranslationValues, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createFormatter } from "./formatter.js";
2
+ export { mergeTranslations } from "./merge.js";
3
+ export { resolveInterfaceLocale } from "./resolve.js";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Merge any number of `TranslationBundle`s into a single immutable
10
+ * registry. Last writer wins at the `(locale, namespace, key)` grain;
11
+ * collisions are reported via the optional `onCollision` callback
12
+ * (called once per replaced key) so callers can wire dev-mode warnings.
13
+ *
14
+ * Associative and deterministic — `merge(a, merge(b, c))` produces the
15
+ * same result as `merge(merge(a, b), c)`. Empty / undefined inputs are
16
+ * accepted and produce identity behaviour, so callers don't need
17
+ * conditional branching when a locale bundle is absent.
18
+ */
19
+ import type { LocaleCode, MessageKey, Namespace, TranslationBundle } from './types.js';
20
+ export interface MergeOptions {
21
+ /**
22
+ * Invoked once per collision (where a later bundle overrides an
23
+ * earlier bundle's value). Pure side effect — used by callers to
24
+ * emit `console.warn` lines in development. Production hosts can
25
+ * pass `undefined` to suppress.
26
+ */
27
+ onCollision?: (collision: TranslationCollision) => void;
28
+ }
29
+ export interface TranslationCollision {
30
+ locale: LocaleCode;
31
+ namespace: Namespace;
32
+ key: MessageKey;
33
+ /** The value that was already present and is being overwritten. */
34
+ previousValue: string;
35
+ /** The value that replaces it. */
36
+ nextValue: string;
37
+ }
38
+ export declare function mergeTranslations(...bundles: Array<TranslationBundle | undefined>): TranslationBundle;
39
+ export declare function mergeTranslations(options: MergeOptions, ...bundles: Array<TranslationBundle | undefined>): TranslationBundle;
package/dist/merge.js ADDED
@@ -0,0 +1,58 @@
1
+ function mergeTranslations(first, ...rest) {
2
+ let options = {};
3
+ let bundles;
4
+ if (isMergeOptions(first)) {
5
+ options = first;
6
+ bundles = rest;
7
+ } else bundles = [
8
+ first,
9
+ ...rest
10
+ ];
11
+ const out = {};
12
+ for (const bundle of bundles)if (null != bundle) for (const locale of Object.keys(bundle)){
13
+ const localeBundle = bundle[locale];
14
+ if (null == localeBundle) continue;
15
+ let targetLocale = out[locale];
16
+ if (null == targetLocale) {
17
+ targetLocale = {};
18
+ out[locale] = targetLocale;
19
+ }
20
+ for (const namespace of Object.keys(localeBundle)){
21
+ const namespaceBundle = localeBundle[namespace];
22
+ if (null == namespaceBundle) continue;
23
+ let targetNs = targetLocale[namespace];
24
+ if (null == targetNs) {
25
+ targetNs = {};
26
+ targetLocale[namespace] = targetNs;
27
+ }
28
+ for (const key of Object.keys(namespaceBundle)){
29
+ const nextValue = namespaceBundle[key];
30
+ if (null == nextValue) continue;
31
+ const previousValue = targetNs[key];
32
+ if (void 0 !== previousValue && previousValue !== nextValue) options.onCollision?.({
33
+ locale,
34
+ namespace,
35
+ key,
36
+ previousValue,
37
+ nextValue
38
+ });
39
+ targetNs[key] = nextValue;
40
+ }
41
+ }
42
+ }
43
+ return freezeBundle(out);
44
+ }
45
+ function isMergeOptions(value) {
46
+ if (null == value || 'object' != typeof value) return false;
47
+ const obj = value;
48
+ return 'function' == typeof obj.onCollision;
49
+ }
50
+ function freezeBundle(bundle) {
51
+ for (const locale of Object.keys(bundle)){
52
+ const localeBundle = bundle[locale];
53
+ for (const namespace of Object.keys(localeBundle))Object.freeze(localeBundle[namespace]);
54
+ Object.freeze(localeBundle);
55
+ }
56
+ return Object.freeze(bundle);
57
+ }
58
+ export { mergeTranslations };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { Formatter } from '../formatter.js';
9
+ import type { LocaleCode, LocaleDefinition, TranslationBundle } from '../types.js';
10
+ export interface I18nContextValue {
11
+ formatter: Formatter;
12
+ bundle: TranslationBundle;
13
+ activeLocale: LocaleCode;
14
+ defaultLocale: LocaleCode;
15
+ /**
16
+ * Permitted locale set with native names. `<LanguageMenu>` renders
17
+ * one row per entry; the resolver in the root package just needs the
18
+ * codes (`localeDefinitions.map(d => d.code)`).
19
+ */
20
+ localeDefinitions: readonly LocaleDefinition[];
21
+ /**
22
+ * Imperative locale change. Provided by the host adapter — typically
23
+ * calls a server fn that updates the user's stored preference and
24
+ * the cookie, then re-renders the provider with the new locale.
25
+ * When undefined the language switcher renders disabled.
26
+ */
27
+ setLocale?: (next: LocaleCode) => void | Promise<void>;
28
+ }
29
+ export declare const I18nContext: import("react").Context<I18nContextValue | null>;
@@ -0,0 +1,3 @@
1
+ import { createContext } from "react";
2
+ const I18nContext = createContext(null);
3
+ export { I18nContext };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import { type ReactNode } from 'react';
9
+ import { type MissingTranslationEvent } from '../formatter.js';
10
+ import type { LocaleCode, LocaleDefinition, TranslationBundle } from '../types.js';
11
+ export interface I18nProviderProps {
12
+ bundle: TranslationBundle;
13
+ activeLocale: LocaleCode;
14
+ defaultLocale: LocaleCode;
15
+ /** Permitted locale set with native names — drives `<LanguageMenu>`. */
16
+ localeDefinitions: readonly LocaleDefinition[];
17
+ /**
18
+ * Optional handler the host wires to its language-switcher server fn.
19
+ * Receives the new locale; expected to persist it and trigger a re-
20
+ * render with the updated `activeLocale` prop.
21
+ */
22
+ setLocale?: (next: LocaleCode) => void | Promise<void>;
23
+ /**
24
+ * Override the dev-time `console.warn` on missing translations.
25
+ * Defaults to a one-shot warn per `(locale, namespace, key)` triple
26
+ * when `process.env.NODE_ENV !== 'production'`.
27
+ */
28
+ onMissing?: (event: MissingTranslationEvent) => void;
29
+ children: ReactNode;
30
+ }
31
+ export declare function I18nProvider({ bundle, activeLocale, defaultLocale, localeDefinitions, setLocale, onMissing, children, }: I18nProviderProps): import("react").JSX.Element;
@@ -0,0 +1,38 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { createFormatter } from "../formatter.js";
4
+ import { I18nContext } from "./i18n-context.js";
5
+ function I18nProvider({ bundle, activeLocale, defaultLocale, localeDefinitions, setLocale, onMissing, children }) {
6
+ const value = useMemo(()=>{
7
+ const onMissingResolved = onMissing ?? ('production' !== process.env.NODE_ENV ? defaultMissingWarner : void 0);
8
+ return {
9
+ formatter: createFormatter({
10
+ bundle,
11
+ activeLocale,
12
+ defaultLocale,
13
+ onMissing: onMissingResolved
14
+ }),
15
+ bundle,
16
+ activeLocale,
17
+ defaultLocale,
18
+ localeDefinitions,
19
+ setLocale
20
+ };
21
+ }, [
22
+ bundle,
23
+ activeLocale,
24
+ defaultLocale,
25
+ localeDefinitions,
26
+ setLocale,
27
+ onMissing
28
+ ]);
29
+ return /*#__PURE__*/ jsx(I18nContext.Provider, {
30
+ value: value,
31
+ children: children
32
+ });
33
+ }
34
+ function defaultMissingWarner(event) {
35
+ if (event.fellThroughToKey) console.warn(`[@byline/i18n] missing translation: ${event.activeLocale}.${event.namespace}.${event.key} — also missing in default locale; rendered raw key.`);
36
+ else console.warn(`[@byline/i18n] missing translation: ${event.activeLocale}.${event.namespace}.${event.key} — using default-locale fallback.`);
37
+ }
38
+ export { I18nProvider };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Single React-side barrel. Provider + hook + language switcher all
10
+ * share one React Context identity by virtue of living behind this
11
+ * single subpath export — splitting across more subpaths would risk
12
+ * Vite `optimizeDeps` pre-bundling each subpath into a private copy of
13
+ * the Context module, breaking provider/consumer identity (see the
14
+ * `@byline/ui/src/react.ts` comment for the same trap in another
15
+ * package).
16
+ */
17
+ export { I18nContext } from './i18n-context.js';
18
+ export { I18nProvider } from './i18n-provider.js';
19
+ export { LanguageMenu } from './language-menu.js';
20
+ export { useTranslation } from './use-translation.js';
21
+ export type { I18nContextValue } from './i18n-context.js';
22
+ export type { I18nProviderProps } from './i18n-provider.js';
23
+ export type { LanguageMenuProps } from './language-menu.js';
24
+ export type { UseTranslationReturn } from './use-translation.js';
@@ -0,0 +1,4 @@
1
+ export { I18nContext } from "./i18n-context.js";
2
+ export { I18nProvider } from "./i18n-provider.js";
3
+ export { LanguageMenu } from "./language-menu.js";
4
+ export { useTranslation } from "./use-translation.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export interface LanguageMenuProps {
9
+ className?: string;
10
+ /** Tailwind / CSS class applied to the icon + label colour. */
11
+ color?: string;
12
+ /** Render the menu disabled (loading, no `setLocale`, etc.). */
13
+ disabled?: boolean;
14
+ }
15
+ export declare function LanguageMenu({ className, color, disabled }: LanguageMenuProps): import("react").JSX.Element | null;
@@ -0,0 +1,78 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useContext, useState } from "react";
3
+ import { CheckIcon, Dropdown, GlobeIcon } from "@byline/ui/react";
4
+ import classnames from "classnames";
5
+ import { I18nContext } from "./i18n-context.js";
6
+ function LanguageMenu({ className, color, disabled }) {
7
+ const context = useContext(I18nContext);
8
+ const [busy, setBusy] = useState(false);
9
+ if (null == context) throw new Error('[@byline/i18n] <LanguageMenu> must be used inside <I18nProvider>. Mount the provider in your admin shell root.');
10
+ const { activeLocale, localeDefinitions, setLocale } = context;
11
+ if (localeDefinitions.length < 2) return null;
12
+ const active = localeDefinitions.find((d)=>d.code === activeLocale);
13
+ const isDisabled = disabled || null == setLocale || busy;
14
+ const handleSelect = async (next)=>{
15
+ if (next === activeLocale || null == setLocale || busy) return;
16
+ setBusy(true);
17
+ try {
18
+ await setLocale(next);
19
+ } finally{
20
+ setBusy(false);
21
+ }
22
+ };
23
+ return /*#__PURE__*/ jsx("div", {
24
+ className: className,
25
+ children: /*#__PURE__*/ jsxs(Dropdown.Root, {
26
+ modal: false,
27
+ children: [
28
+ /*#__PURE__*/ jsxs(Dropdown.Trigger, {
29
+ render: /*#__PURE__*/ jsx("button", {
30
+ type: "button",
31
+ "aria-label": active?.nativeName ?? activeLocale,
32
+ disabled: isDisabled,
33
+ className: "component--byline-language-menu rounded flex items-center justify-between gap-1 outline-none disabled:opacity-50"
34
+ }),
35
+ children: [
36
+ /*#__PURE__*/ jsx(GlobeIcon, {
37
+ svgClassName: color
38
+ }),
39
+ /*#__PURE__*/ jsx("span", {
40
+ className: classnames(color, 'hidden sm:inline mr-[4px]'),
41
+ children: active?.nativeName ?? activeLocale
42
+ })
43
+ ]
44
+ }),
45
+ /*#__PURE__*/ jsx(Dropdown.Portal, {
46
+ children: /*#__PURE__*/ jsx(Dropdown.Content, {
47
+ align: "center",
48
+ sideOffset: 10,
49
+ className: classnames('z-40 rounded radix-side-bottom:animate-slide-down radix-side-top:animate-slide-up', 'w-32 px-1.5 py-1 shadow-md', 'bg-white dark:bg-canvas-800 border dark:border-canvas-700 shadow'),
50
+ children: localeDefinitions.map((def)=>{
51
+ const isActive = def.code === activeLocale;
52
+ return /*#__PURE__*/ jsx(Dropdown.Item, {
53
+ onClick: ()=>handleSelect(def.code),
54
+ children: /*#__PURE__*/ jsxs("div", {
55
+ className: "flex",
56
+ children: [
57
+ /*#__PURE__*/ jsx("span", {
58
+ className: "inline-block w-[22px]",
59
+ children: isActive && /*#__PURE__*/ jsx(CheckIcon, {
60
+ width: "18px",
61
+ height: "18px"
62
+ })
63
+ }),
64
+ /*#__PURE__*/ jsx("span", {
65
+ className: "text-left inline-block w-full flex-1 self-start text-black dark:text-gray-300",
66
+ children: def.nativeName
67
+ })
68
+ ]
69
+ })
70
+ }, def.code);
71
+ })
72
+ })
73
+ })
74
+ ]
75
+ })
76
+ });
77
+ }
78
+ export { LanguageMenu };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { LocaleCode, MessageKey, Namespace, TranslationValues } from '../types.js';
9
+ export interface UseTranslationReturn {
10
+ /** Format `namespace.key` against the provider's active locale. */
11
+ t: (key: MessageKey, values?: TranslationValues) => string;
12
+ /** The active locale — useful for `<html lang>` and direction logic. */
13
+ locale: LocaleCode;
14
+ }
15
+ /**
16
+ * Bind to a single namespace for the lifetime of the component. The
17
+ * returned `t` resolves keys against the active locale with default-
18
+ * locale fallback baked in (see `createFormatter` for the cascade).
19
+ *
20
+ * Throws if called outside `<I18nProvider>` — the loud failure is
21
+ * deliberate. A silently-broken admin shell with raw keys on screen
22
+ * is harder to notice than a thrown error during development.
23
+ */
24
+ export declare function useTranslation(namespace: Namespace): UseTranslationReturn;
@@ -0,0 +1,16 @@
1
+ import { useContext, useMemo } from "react";
2
+ import { I18nContext } from "./i18n-context.js";
3
+ function useTranslation(namespace) {
4
+ const context = useContext(I18nContext);
5
+ if (null == context) throw new Error('[@byline/i18n] useTranslation must be used inside <I18nProvider>. Mount the provider in your admin shell root.');
6
+ const { formatter, activeLocale } = context;
7
+ return useMemo(()=>({
8
+ t: (key, values)=>formatter.t(namespace, key, values),
9
+ locale: activeLocale
10
+ }), [
11
+ formatter,
12
+ namespace,
13
+ activeLocale
14
+ ]);
15
+ }
16
+ export { useTranslation };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { LocaleCode } from './types.js';
9
+ export interface ResolveInterfaceLocaleOptions {
10
+ /** Permitted locale set — from `i18n.interface.locales`. */
11
+ locales: readonly LocaleCode[];
12
+ /** Last-resort fallback — from `i18n.interface.defaultLocale`. */
13
+ defaultLocale: LocaleCode;
14
+ /** `admin_users.preferred_locale` for the authenticated request, if any. */
15
+ preferred?: LocaleCode | null;
16
+ /** Value of the `byline_admin_lng` cookie. */
17
+ cookie?: string | null;
18
+ /** Raw `Accept-Language` request header. */
19
+ acceptLanguage?: string | null;
20
+ }
21
+ export declare function resolveInterfaceLocale(options: ResolveInterfaceLocaleOptions): LocaleCode;
@@ -0,0 +1,28 @@
1
+ import { match } from "@formatjs/intl-localematcher";
2
+ import negotiator_0 from "negotiator";
3
+ function resolveInterfaceLocale(options) {
4
+ const { locales, defaultLocale, preferred, cookie, acceptLanguage } = options;
5
+ if (null != preferred && locales.includes(preferred)) return preferred;
6
+ if (null != cookie && locales.includes(cookie)) return cookie;
7
+ if (null != acceptLanguage && acceptLanguage.length > 0) {
8
+ const matched = negotiateAcceptLanguage(acceptLanguage, locales, defaultLocale);
9
+ if (null != matched) return matched;
10
+ }
11
+ return defaultLocale;
12
+ }
13
+ function negotiateAcceptLanguage(header, locales, defaultLocale) {
14
+ try {
15
+ const negotiator = new negotiator_0({
16
+ headers: {
17
+ 'accept-language': header
18
+ }
19
+ });
20
+ const requested = negotiator.languages();
21
+ if (0 === requested.length) return null;
22
+ const matched = match(requested, locales, defaultLocale);
23
+ return locales.includes(matched) ? matched : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ export { resolveInterfaceLocale };