@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.
- package/LICENSE +373 -0
- package/README.md +166 -0
- package/dist/admin/en.js +424 -0
- package/dist/admin/fr.js +424 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.js +27 -0
- package/dist/formatter.d.ts +40 -0
- package/dist/formatter.js +65 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +3 -0
- package/dist/merge.d.ts +39 -0
- package/dist/merge.js +58 -0
- package/dist/react/i18n-context.d.ts +29 -0
- package/dist/react/i18n-context.js +3 -0
- package/dist/react/i18n-provider.d.ts +31 -0
- package/dist/react/i18n-provider.js +38 -0
- package/dist/react/index.d.ts +24 -0
- package/dist/react/index.js +4 -0
- package/dist/react/language-menu.d.ts +15 -0
- package/dist/react/language-menu.js +78 -0
- package/dist/react/use-translation.d.ts +24 -0
- package/dist/react/use-translation.js +16 -0
- package/dist/resolve.d.ts +21 -0
- package/dist/resolve.js +28 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/package.json +100 -0
- package/src/admin/en.json +423 -0
- package/src/admin/fr.json +423 -0
- package/src/admin/index.test.node.ts +68 -0
- package/src/admin/index.ts +99 -0
- package/src/formatter.test.node.ts +163 -0
- package/src/formatter.ts +166 -0
- package/src/index.ts +47 -0
- package/src/merge.test.node.ts +85 -0
- package/src/merge.ts +135 -0
- package/src/react/i18n-context.ts +45 -0
- package/src/react/i18n-provider.tsx +76 -0
- package/src/react/index.ts +26 -0
- package/src/react/language-menu.tsx +115 -0
- package/src/react/use-translation.ts +45 -0
- package/src/resolve.test.node.ts +128 -0
- package/src/resolve.ts +84 -0
- 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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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
package/dist/merge.d.ts
ADDED
|
@@ -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,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,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;
|
package/dist/resolve.js
ADDED
|
@@ -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 };
|