@open-mercato/shared 0.6.8-develop.7000.1.0ae682bad1 → 0.6.8-develop.7003.2.24fef49d48

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/i18n/context.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport { createContext, useContext, useMemo, type ReactNode } from 'react'\nimport type { Locale } from './config'\n\nexport type Dict = Record<string, string>\n\nexport type TranslateParams = Record<string, string | number>\n\nexport type TranslateFn = (\n key: string,\n fallbackOrParams?: string | TranslateParams,\n params?: TranslateParams\n) => string\n\nexport type I18nContextValue = {\n locale: Locale\n t: TranslateFn\n /** True when the locale is pinned via `OM_FORCE_LOCALE`; UI should hide switchers. */\n localeLocked: boolean\n}\n\nconst I18N_CONTEXT_KEY = '__openMercatoI18nContext'\n\ntype GlobalI18nContextStore = typeof globalThis & {\n [I18N_CONTEXT_KEY]?: ReturnType<typeof createContext<I18nContextValue | null>>\n}\n\nfunction getI18nContext() {\n const store = globalThis as GlobalI18nContextStore\n if (!store[I18N_CONTEXT_KEY]) {\n store[I18N_CONTEXT_KEY] = createContext<I18nContextValue | null>(null)\n }\n return store[I18N_CONTEXT_KEY]\n}\n\nconst I18nContext = getI18nContext()\n\nfunction format(template: string, params?: TranslateParams) {\n if (!params) return template\n return template.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (_, doubleKey, singleKey) => {\n const key = doubleKey ?? singleKey\n if (!key) return _\n const value = params[key]\n if (value === undefined) {\n return doubleKey ? `{{${key}}}` : `{${key}}`\n }\n return String(value)\n })\n}\n\nexport function I18nProvider({ children, locale, dict, localeLocked = false }: { children: ReactNode; locale: Locale; dict: Dict; localeLocked?: boolean }) {\n const value = useMemo<I18nContextValue>(() => ({\n locale,\n localeLocked,\n t: (key, fallbackOrParams, params) => {\n let fallback: string | undefined\n let resolvedParams: TranslateParams | undefined\n\n if (typeof fallbackOrParams === 'string') {\n fallback = fallbackOrParams\n resolvedParams = params\n } else {\n resolvedParams = fallbackOrParams ?? params\n }\n\n const template = dict[key] ?? fallback ?? key\n return format(template, resolvedParams)\n },\n }), [locale, dict, localeLocked])\n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>\n}\n\nexport function useT() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useT must be used within I18nProvider')\n return ctx.t\n}\n\n/**\n * Like `useT`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use where a translator is desirable but not\n * guaranteed (e.g. plumbing `t` into side-effect handlers that may run before\n * the provider mounts) \u2014 callers MUST provide a fallback.\n */\nexport function useOptionalT(): TranslateFn | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.t\n}\n\nexport function useLocale() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useLocale must be used within I18nProvider')\n return ctx.locale\n}\n\n/**\n * Like `useLocale`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use in shared components that receive their\n * translator as a prop and may render outside a provider (galleries, tests).\n */\nexport function useOptionalLocale(): Locale | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.locale\n}\n\n/**\n * True when the active locale is pinned via `OM_FORCE_LOCALE`. Returns `false`\n * when no provider is in scope so callers can render unconditionally.\n */\nexport function useLocaleLocked() {\n const ctx = useContext(I18nContext)\n return ctx?.localeLocked ?? false\n}\n"],
5
- "mappings": ";AAqES;AApET,SAAS,eAAe,YAAY,eAA+B;AAoBnE,MAAM,mBAAmB;AAMzB,SAAS,iBAAiB;AACxB,QAAM,QAAQ;AACd,MAAI,CAAC,MAAM,gBAAgB,GAAG;AAC5B,UAAM,gBAAgB,IAAI,cAAuC,IAAI;AAAA,EACvE;AACA,SAAO,MAAM,gBAAgB;AAC/B;AAEA,MAAM,cAAc,eAAe;AAEnC,SAAS,OAAO,UAAkB,QAA0B;AAC1D,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,SAAS,QAAQ,4BAA4B,CAAC,GAAG,WAAW,cAAc;AAC/E,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,UAAU,QAAW;AACvB,aAAO,YAAY,KAAK,GAAG,OAAO,IAAI,GAAG;AAAA,IAC3C;AACA,WAAO,OAAO,KAAK;AAAA,EACrB,CAAC;AACH;AAEO,SAAS,aAAa,EAAE,UAAU,QAAQ,MAAM,eAAe,MAAM,GAAgF;AAC1J,QAAM,QAAQ,QAA0B,OAAO;AAAA,IAC7C;AAAA,IACA;AAAA,IACA,GAAG,CAAC,KAAK,kBAAkB,WAAW;AACpC,UAAI;AACJ,UAAI;AAEJ,UAAI,OAAO,qBAAqB,UAAU;AACxC,mBAAW;AACX,yBAAiB;AAAA,MACnB,OAAO;AACL,yBAAiB,oBAAoB;AAAA,MACvC;AAEA,YAAM,WAAW,KAAK,GAAG,KAAK,YAAY;AAC1C,aAAO,OAAO,UAAU,cAAc;AAAA,IACxC;AAAA,EACF,IAAI,CAAC,QAAQ,MAAM,YAAY,CAAC;AAChC,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAEO,SAAS,OAAO;AACrB,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uCAAuC;AACjE,SAAO,IAAI;AACb;AAQO,SAAS,eAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAEO,SAAS,YAAY;AAC1B,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,4CAA4C;AACtE,SAAO,IAAI;AACb;AAOO,SAAS,oBAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAMO,SAAS,kBAAkB;AAChC,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK,gBAAgB;AAC9B;",
4
+ "sourcesContent": ["\"use client\"\nimport { createContext, useContext, useMemo, type PropsWithChildren } from 'react'\nimport type { Locale } from './config'\n\nexport type Dict = Record<string, string>\n\nexport type TranslateParams = Record<string, string | number>\n\nexport type TranslateFn = (\n key: string,\n fallbackOrParams?: string | TranslateParams,\n params?: TranslateParams\n) => string\n\nexport type I18nContextValue = {\n locale: Locale\n t: TranslateFn\n /** True when the locale is pinned via `OM_FORCE_LOCALE`; UI should hide switchers. */\n localeLocked: boolean\n}\n\nconst I18N_CONTEXT_KEY = '__openMercatoI18nContext'\n\ntype GlobalI18nContextStore = typeof globalThis & {\n [I18N_CONTEXT_KEY]?: ReturnType<typeof createContext<I18nContextValue | null>>\n}\n\nfunction getI18nContext() {\n const store = globalThis as GlobalI18nContextStore\n if (!store[I18N_CONTEXT_KEY]) {\n store[I18N_CONTEXT_KEY] = createContext<I18nContextValue | null>(null)\n }\n return store[I18N_CONTEXT_KEY]\n}\n\nconst I18nContext = getI18nContext()\n\nfunction format(template: string, params?: TranslateParams) {\n if (!params) return template\n return template.replace(/\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g, (_, doubleKey, singleKey) => {\n const key = doubleKey ?? singleKey\n if (!key) return _\n const value = params[key]\n if (value === undefined) {\n return doubleKey ? `{{${key}}}` : `{${key}}`\n }\n return String(value)\n })\n}\n\nexport function I18nProvider({ children, locale, dict, localeLocked = false }: PropsWithChildren<{ locale: Locale; dict: Dict; localeLocked?: boolean }>) {\n const value = useMemo<I18nContextValue>(() => ({\n locale,\n localeLocked,\n t: (key, fallbackOrParams, params) => {\n let fallback: string | undefined\n let resolvedParams: TranslateParams | undefined\n\n if (typeof fallbackOrParams === 'string') {\n fallback = fallbackOrParams\n resolvedParams = params\n } else {\n resolvedParams = fallbackOrParams ?? params\n }\n\n const template = dict[key] ?? fallback ?? key\n return format(template, resolvedParams)\n },\n }), [locale, dict, localeLocked])\n return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>\n}\n\nexport function useT() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useT must be used within I18nProvider')\n return ctx.t\n}\n\n/**\n * Like `useT`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use where a translator is desirable but not\n * guaranteed (e.g. plumbing `t` into side-effect handlers that may run before\n * the provider mounts) \u2014 callers MUST provide a fallback.\n */\nexport function useOptionalT(): TranslateFn | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.t\n}\n\nexport function useLocale() {\n const ctx = useContext(I18nContext)\n if (!ctx) throw new Error('useLocale must be used within I18nProvider')\n return ctx.locale\n}\n\n/**\n * Like `useLocale`, but returns `undefined` instead of throwing when no\n * `I18nProvider` is in scope. Use in shared components that receive their\n * translator as a prop and may render outside a provider (galleries, tests).\n */\nexport function useOptionalLocale(): Locale | undefined {\n const ctx = useContext(I18nContext)\n return ctx?.locale\n}\n\n/**\n * True when the active locale is pinned via `OM_FORCE_LOCALE`. Returns `false`\n * when no provider is in scope so callers can render unconditionally.\n */\nexport function useLocaleLocked() {\n const ctx = useContext(I18nContext)\n return ctx?.localeLocked ?? false\n}\n"],
5
+ "mappings": ";AAqES;AApET,SAAS,eAAe,YAAY,eAAuC;AAoB3E,MAAM,mBAAmB;AAMzB,SAAS,iBAAiB;AACxB,QAAM,QAAQ;AACd,MAAI,CAAC,MAAM,gBAAgB,GAAG;AAC5B,UAAM,gBAAgB,IAAI,cAAuC,IAAI;AAAA,EACvE;AACA,SAAO,MAAM,gBAAgB;AAC/B;AAEA,MAAM,cAAc,eAAe;AAEnC,SAAS,OAAO,UAAkB,QAA0B;AAC1D,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,SAAS,QAAQ,4BAA4B,CAAC,GAAG,WAAW,cAAc;AAC/E,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,UAAU,QAAW;AACvB,aAAO,YAAY,KAAK,GAAG,OAAO,IAAI,GAAG;AAAA,IAC3C;AACA,WAAO,OAAO,KAAK;AAAA,EACrB,CAAC;AACH;AAEO,SAAS,aAAa,EAAE,UAAU,QAAQ,MAAM,eAAe,MAAM,GAA8E;AACxJ,QAAM,QAAQ,QAA0B,OAAO;AAAA,IAC7C;AAAA,IACA;AAAA,IACA,GAAG,CAAC,KAAK,kBAAkB,WAAW;AACpC,UAAI;AACJ,UAAI;AAEJ,UAAI,OAAO,qBAAqB,UAAU;AACxC,mBAAW;AACX,yBAAiB;AAAA,MACnB,OAAO;AACL,yBAAiB,oBAAoB;AAAA,MACvC;AAEA,YAAM,WAAW,KAAK,GAAG,KAAK,YAAY;AAC1C,aAAO,OAAO,UAAU,cAAc;AAAA,IACxC;AAAA,EACF,IAAI,CAAC,QAAQ,MAAM,YAAY,CAAC;AAChC,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAEO,SAAS,OAAO;AACrB,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uCAAuC;AACjE,SAAO,IAAI;AACb;AAQO,SAAS,eAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAEO,SAAS,YAAY;AAC1B,QAAM,MAAM,WAAW,WAAW;AAClC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,4CAA4C;AACtE,SAAO,IAAI;AACb;AAOO,SAAS,oBAAwC;AACtD,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK;AACd;AAMO,SAAS,kBAAkB;AAChC,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO,KAAK,gBAAgB;AAC9B;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.8-develop.7000.1.0ae682bad1";
1
+ const APP_VERSION = "0.6.8-develop.7003.2.24fef49d48";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.7000.1.0ae682bad1';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.7003.2.24fef49d48';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.8-develop.7000.1.0ae682bad1",
3
+ "version": "0.6.8-develop.7003.2.24fef49d48",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -105,7 +105,7 @@
105
105
  "@mikro-orm/core": "^7.1.8",
106
106
  "@mikro-orm/decorators": "^7.1.8",
107
107
  "@mikro-orm/postgresql": "^7.1.8",
108
- "@open-mercato/cache": "0.6.8-develop.7000.1.0ae682bad1",
108
+ "@open-mercato/cache": "0.6.8-develop.7003.2.24fef49d48",
109
109
  "@types/sanitize-html": "^2.16.1",
110
110
  "dotenv": "^17.4.2",
111
111
  "pino": "^10.3.1",
@@ -0,0 +1,26 @@
1
+ import * as React from 'react'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { I18nProvider, useT } from '../context'
4
+
5
+ function Greeting() {
6
+ const t = useT()
7
+ return React.createElement('span', null, t('greeting', 'hello'))
8
+ }
9
+
10
+ describe('I18nProvider', () => {
11
+ it('accepts children positionally via React.createElement, matching props without a children key', () => {
12
+ const element = React.createElement(
13
+ I18nProvider,
14
+ { locale: 'en', dict: {} },
15
+ React.createElement(Greeting),
16
+ )
17
+
18
+ expect(renderToStaticMarkup(element)).toContain('hello')
19
+ })
20
+
21
+ it('renders with no children at all', () => {
22
+ const element = React.createElement(I18nProvider, { locale: 'en', dict: {} })
23
+
24
+ expect(renderToStaticMarkup(element)).toBe('')
25
+ })
26
+ })
@@ -1,5 +1,5 @@
1
1
  "use client"
2
- import { createContext, useContext, useMemo, type ReactNode } from 'react'
2
+ import { createContext, useContext, useMemo, type PropsWithChildren } from 'react'
3
3
  import type { Locale } from './config'
4
4
 
5
5
  export type Dict = Record<string, string>
@@ -48,7 +48,7 @@ function format(template: string, params?: TranslateParams) {
48
48
  })
49
49
  }
50
50
 
51
- export function I18nProvider({ children, locale, dict, localeLocked = false }: { children: ReactNode; locale: Locale; dict: Dict; localeLocked?: boolean }) {
51
+ export function I18nProvider({ children, locale, dict, localeLocked = false }: PropsWithChildren<{ locale: Locale; dict: Dict; localeLocked?: boolean }>) {
52
52
  const value = useMemo<I18nContextValue>(() => ({
53
53
  locale,
54
54
  localeLocked,
@@ -0,0 +1,36 @@
1
+ // Compile-time-only regression guard for issue #5155. Never imported at
2
+ // runtime: `yarn typecheck` (`tsc --noEmit`) is the only gate that can catch
3
+ // a regression here, because this repo's Jest transform runs with
4
+ // `isolatedModules: true` and therefore skips type diagnostics — a test in
5
+ // `__tests__/context.test.tsx` would keep passing even if `children` became
6
+ // required again.
7
+ //
8
+ // It sits outside `__tests__` because `packages/shared/tsconfig.json` excludes
9
+ // that directory from `tsc --noEmit`, and it is a `.tsx` rather than a `.ts`
10
+ // because the mutation gate mutates changed `src/lib/**/*.ts` files and runs
11
+ // only their related Jest tests (`enableFindRelatedTests`). A file that exists
12
+ // purely for the type checker has no related tests by construction, so Stryker
13
+ // aborts the run with "No tests were executed"; `.tsx` is already out of that
14
+ // scope by design — see `scripts/stryker/scope.mjs`.
15
+ import * as React from 'react'
16
+ import { I18nProvider } from './context'
17
+
18
+ function acceptsChildrenPositionally() {
19
+ return React.createElement(I18nProvider, { locale: 'en', dict: {} }, <span />)
20
+ }
21
+
22
+ function acceptsNoChildren() {
23
+ return React.createElement(I18nProvider, { locale: 'en', dict: {} })
24
+ }
25
+
26
+ function acceptsChildrenAsJsx() {
27
+ return (
28
+ <I18nProvider locale="en" dict={{}}>
29
+ <span />
30
+ </I18nProvider>
31
+ )
32
+ }
33
+
34
+ void acceptsChildrenPositionally
35
+ void acceptsNoChildren
36
+ void acceptsChildrenAsJsx