@let-value/translate-react 1.2.4 → 1.2.5
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/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/context.ts","../src/components/LocaleProvider.ts","../src/dehydration.ts","../src/hooks/useTranslations.ts","../src/utils.ts","../src/components/Message.ts","../src/components/Plural.ts","../src/translatorCache.ts","../src/components/TranslationsProvider.ts","../src/hooks/useLocale.ts"],"sourcesContent":["import type { Locale, Translator } from \"@let-value/translate\";\nimport { createContext } from \"react\";\n\ntype AnyTranslator = Translator<any>;\n\n/**\n * A provider's link in the dehydration chain, walked from `useTranslations` so a\n * consumer can seed itself — and every translator it merges against — from the\n * server payload before it would otherwise suspend.\n *\n * Only providers with a lazily-loaded catalog add a link; sync ones are simply\n * skipped, so a child links straight to its nearest lazy ancestor.\n */\nexport interface DehydrationEntry {\n /** `useId()` of the provider, matching the inlined script's attribute. */\n id: string;\n translator: AnyTranslator;\n /** Sorted locale keys of the provider's `translations` prop. */\n keys: string[];\n parent: DehydrationEntry | undefined;\n}\n\nexport const localeContext = createContext<Locale | undefined>(undefined);\nexport const translatorContext = createContext<AnyTranslator | undefined>(undefined);\nexport const dehydrationContext = createContext<DehydrationEntry | undefined>(undefined);\n","import type { Locale } from \"@let-value/translate\";\nimport { createElement, type ReactNode } from \"react\";\nimport { localeContext } from \"../context.ts\";\n\nexport interface LocaleProviderProps {\n locale: Locale;\n children?: ReactNode;\n}\n\nexport function LocaleProvider({ locale, children }: LocaleProviderProps) {\n return createElement(localeContext.Provider, { value: locale }, children);\n}\n","import type { Locale, Translator } from \"@let-value/translate\";\nimport type { GetTextTranslations } from \"gettext-parser\";\nimport type { DehydrationEntry } from \"./context.ts\";\nimport type { TranslationsMap } from \"./translatorCache.ts\";\n\n/**\n * Attribute that marks an inlined catalog. The value is the `useId()` of the\n * `TranslationsProvider` that emitted it — React guarantees that id is identical\n * between the server render and the hydration render of the same tree position.\n */\nexport const DEHYDRATION_ATTRIBUTE = \"data-translations\";\n\ninterface DehydratedPayload {\n /** Sorted locale keys of the provider's `translations` prop. */\n k: string[];\n /** Locale the catalog was resolved for. */\n l: string;\n /** The catalog itself. */\n c: GetTextTranslations;\n}\n\n/** Locale keys of a `translations` map, sorted so server and client agree. */\nexport function localeKeys(translations: TranslationsMap): string[] {\n return Object.keys(translations).sort();\n}\n\n/** True when resolving `locale` from `translations` requires awaiting a loader. */\nexport function isAsyncLocale(translations: TranslationsMap, locale: Locale | undefined): boolean {\n if (!locale) return false;\n const entry = translations[locale];\n if (!entry) return false;\n return typeof entry === \"function\" || \"then\" in entry;\n}\n\n/**\n * `</script` is the only sequence that can terminate the script element early;\n * escaping `<` as a JSON unicode escape keeps the text byte-identical to what\n * `textContent` yields on the client, so hydration matches exactly.\n */\nfunction encode(payload: DehydratedPayload): string {\n return JSON.stringify(payload).replaceAll(\"<\", \"\\\\u003c\");\n}\n\nexport function dehydrate(\n keys: string[],\n locale: Locale,\n catalog: GetTextTranslations | undefined,\n): string | undefined {\n if (!catalog) return undefined;\n return encode({ k: keys, l: locale, c: catalog });\n}\n\n/** Locales already seeded from the document, so a hit costs one DOM query. */\nconst seeded = new WeakMap<Translator, Set<string>>();\n\nfunction markSeeded(translator: Translator, locale: Locale): void {\n let locales = seeded.get(translator);\n if (!locales) {\n locales = new Set();\n seeded.set(translator, locales);\n }\n locales.add(locale);\n}\n\nconst warned = new Set<string>();\n\n/**\n * Two React roots on one page share the `useId` sequence unless they were given\n * distinct `identifierPrefix` values, so their providers can emit scripts under\n * the same id. There is no way to tell from render which one belongs to the tree\n * currently hydrating, so refuse to guess: seeding is skipped and the catalog\n * loads lazily, exactly as it did before this mechanism existed.\n */\nfunction isAmbiguous(id: string, count: number): boolean {\n if (count <= 1) return false;\n if (!warned.has(id)) {\n warned.add(id);\n console.warn(\n `Found ${count} inlined translation catalogs for id \"${id}\". ` +\n \"Give each React root a distinct `identifierPrefix` so their ids cannot collide. \" +\n \"Falling back to loading the catalog.\",\n );\n }\n return true;\n}\n\n/**\n * Seed one provider's translator from the catalog the server inlined for it.\n *\n * Returns the raw payload text when one was applied, so the emitter can render an\n * identical `<script>` back and keep hydration byte-for-byte stable.\n *\n * `useId` is stable across a server render and its hydration, but a provider\n * mounted later by client-side navigation gets a fresh id that could collide with\n * an id from the server tree. The locale keys in the payload guard against that:\n * a mismatch is treated as a miss, never as a match.\n */\nexport function readPayload(entry: DehydrationEntry, locale: Locale): string | undefined {\n if (typeof document === \"undefined\") return undefined;\n if (seeded.get(entry.translator)?.has(locale)) return undefined;\n\n const scripts = document.querySelectorAll(`script[${DEHYDRATION_ATTRIBUTE}=\"${entry.id}\"]`);\n if (isAmbiguous(entry.id, scripts.length)) return undefined;\n\n const text = scripts[0]?.textContent;\n if (!text) return undefined;\n\n let payload: DehydratedPayload;\n try {\n payload = JSON.parse(text) as DehydratedPayload;\n } catch {\n return undefined;\n }\n\n if (payload.l !== locale) return undefined;\n if (payload.k.length !== entry.keys.length || payload.k.some((key, index) => key !== entry.keys[index])) {\n return undefined;\n }\n\n entry.translator.prime(locale, payload.c);\n markSeeded(entry.translator, locale);\n return text;\n}\n\n/**\n * Seed a whole provider chain, outermost first — a child's `getLocale` merges\n * against its parent, so the parent has to be resolved by the time the child is.\n *\n * Called from `useTranslations` before it touches `fetchLocale`, which is what\n * lets the app put its Suspense boundary anywhere: the consumer seeds itself at\n * the exact moment it renders, whether that is with the provider or inside a\n * boundary that hydrates much later.\n */\nexport function seedFromDocument(entry: DehydrationEntry | undefined, locale: Locale): void {\n if (!entry) return;\n seedFromDocument(entry.parent, locale);\n readPayload(entry, locale);\n}\n","import type { Locale, LocaleTranslator } from \"@let-value/translate\";\nimport { use } from \"react\";\n\nimport { dehydrationContext, localeContext, translatorContext } from \"../context.ts\";\nimport { seedFromDocument } from \"../dehydration.ts\";\n\n/** @deprecated replace with `use` from react */\nfunction getPromiseState(promise: any) {\n switch (promise.status) {\n case \"pending\":\n return { status: \"pending\" };\n case \"fulfilled\":\n return { status: \"fulfilled\", value: promise.value };\n case \"rejected\":\n return { status: \"rejected\", reason: promise.reason };\n default: {\n promise.status = \"pending\";\n promise.then((value: unknown) => {\n promise.status = \"fulfilled\";\n promise.value = value;\n });\n promise.catch((reason: unknown) => {\n promise.status = \"rejected\";\n promise.reason = reason;\n });\n return getPromiseState(promise);\n }\n }\n}\n\nexport function useTranslations(locale?: Locale): LocaleTranslator {\n const requestedLocale = locale ?? use(localeContext) ?? (\"unknown\" as never);\n const translator = use(translatorContext);\n const dehydrated = use(dehydrationContext);\n if (!translator) {\n throw new Error(\"TranslationsProvider is missing\");\n }\n\n // Seed before `fetchLocale`, so a catalog the server already inlined never\n // starts its loader — the chunk is not fetched and this render does not\n // suspend, which is what lets React keep the server markup.\n seedFromDocument(dehydrated, requestedLocale);\n\n const resource = translator.fetchLocale(requestedLocale);\n if (!(resource instanceof Promise)) {\n return resource;\n }\n\n const state = getPromiseState(resource);\n if (state.status === \"pending\") {\n throw resource;\n }\n if (state.status === \"rejected\") {\n throw state.reason;\n }\n if (state.status === \"fulfilled\") {\n return state.value;\n }\n\n return use(resource);\n}\n","import { Children, Fragment, isValidElement, type PropsWithChildren, type ReactNode } from \"react\";\n\nexport function buildTemplateFromChildren(children: ReactNode): {\n strings: string[];\n values: ReactNode[];\n} {\n function flatten(nodes: ReactNode): ReactNode[] {\n const result: ReactNode[] = [];\n Children.forEach(nodes, (child) => {\n if (isValidElement(child) && child.type === Fragment) {\n result.push(...flatten((child.props as PropsWithChildren).children));\n } else {\n result.push(child);\n }\n });\n return result;\n }\n\n const array = flatten(children);\n const strings: string[] = [\"\"];\n const values: ReactNode[] = [];\n let expectValue = false;\n\n array.forEach((child) => {\n if (typeof child === \"string\") {\n if (expectValue) {\n values.push(child);\n strings.push(\"\");\n expectValue = false;\n } else {\n strings[strings.length - 1] += child;\n expectValue = true;\n }\n } else if (typeof child === \"number\" || child != null) {\n values.push(child as ReactNode);\n strings.push(\"\");\n expectValue = false;\n }\n });\n\n return { strings, values };\n}\n","import { message } from \"@let-value/translate\";\nimport { createElement, Fragment, type ReactNode } from \"react\";\n\nimport { useTranslations } from \"../hooks/useTranslations.ts\";\nimport { buildTemplateFromChildren } from \"../utils.ts\";\n\nexport interface MessageProps {\n context?: string;\n children: ReactNode;\n}\n\nexport function Message({ context, children }: MessageProps) {\n const translator = useTranslations();\n const { strings, values } = buildTemplateFromChildren(children);\n\n if (values.length === 0) {\n const input = strings.join(\"\");\n if (context) {\n return translator.context(context as \"\").message(input as never);\n }\n return translator.message(input as never);\n }\n\n const tokens = values.map((_, i) => `\\u0000${i}\\u0000`);\n const built = message(strings as unknown as TemplateStringsArray, ...tokens);\n const translated = context\n ? translator.translate({ context: context as \"\", id: built })\n : translator.translate(built);\n\n // oxlint-disable-next-line no-control-regex -- using null separators\n const parts = translated.split(/\\u0000(\\d+)\\u0000/);\n const result: ReactNode[] = [];\n for (let i = 0; i < parts.length; i += 2) {\n result.push(parts[i]);\n const idx = parts[i + 1];\n if (idx !== undefined) {\n result.push(values[Number(idx)]);\n }\n }\n\n return createElement(Fragment, null, ...result);\n}\n","import { message, plural } from \"@let-value/translate\";\nimport { createElement, Fragment, type ReactNode } from \"react\";\n\nimport { useTranslations } from \"../hooks/useTranslations.ts\";\nimport { buildTemplateFromChildren } from \"../utils.ts\";\n\nexport interface PluralProps {\n number: number;\n forms: readonly ReactNode[];\n context?: string;\n}\n\nexport function Plural({ number, forms, context }: PluralProps) {\n const translator = useTranslations();\n\n const built = forms.map((child, i) => {\n const { strings, values } = buildTemplateFromChildren(child);\n const tokens = values.map((_, j) => `\\u0000${i}-${j}\\u0000`);\n return { message: message(strings as unknown as TemplateStringsArray, ...tokens), values };\n });\n\n const messages = built.map((b) => b.message);\n const input = plural(...messages, number);\n\n const translated = context\n ? translator.translate({ context: context as \"\", id: input })\n : translator.translate(input);\n\n // oxlint-disable-next-line no-control-regex -- using null separators\n const parts = translated.split(/\\u0000(\\d+)-(\\d+)\\u0000/);\n const result: ReactNode[] = [];\n for (let i = 0; i < parts.length; ) {\n result.push(parts[i]);\n if (i + 2 < parts.length) {\n const formIndex = Number(parts[i + 1]);\n const valueIndex = Number(parts[i + 2]);\n result.push(built[formIndex].values[valueIndex]);\n }\n i += 3;\n }\n\n return createElement(Fragment, null, ...result);\n}\n","import type { Locale, TranslationEntry } from \"@let-value/translate\";\nimport { Translator } from \"@let-value/translate\";\n\nexport type TranslationsMap = Partial<Record<Locale, TranslationEntry>>;\n\n// Fast path: WeakMap keyed on the translations object reference.\n// GC-safe — entries are collected when the object is no longer reachable.\nconst cacheNoParent = new WeakMap<object, Translator>();\nconst cacheWithParent = new WeakMap<object, WeakMap<Translator, Translator>>();\n\nfunction getFromWeakMap(translations: TranslationsMap, parent: Translator | undefined): Translator | undefined {\n if (!parent) return cacheNoParent.get(translations);\n return cacheWithParent.get(translations)?.get(parent);\n}\n\nfunction setInWeakMap(translations: TranslationsMap, parent: Translator | undefined, translator: Translator): void {\n if (!parent) {\n cacheNoParent.set(translations, translator);\n return;\n }\n let byParent = cacheWithParent.get(translations);\n if (!byParent) {\n byParent = new WeakMap();\n cacheWithParent.set(translations, byParent);\n }\n byParent.set(parent, translator);\n}\n\n// Slow path: bounded structural cache for dynamically-created translations objects\n// (e.g. `translations={{ en: data }}` — new wrapper object, stable value references).\n// Bounded to cap memory usage in infinite-render scenarios.\nconst MAX_STRUCTURAL_ENTRIES = 32;\n\ntype SortedEntry = [Locale, TranslationEntry];\n\ninterface StructuralEntry {\n sortedEntries: SortedEntry[];\n parent: Translator | undefined;\n translator: Translator;\n}\n\nconst structuralCache: StructuralEntry[] = [];\n\nfunction toSortedEntries(translations: TranslationsMap): SortedEntry[] {\n return (Object.entries(translations) as SortedEntry[]).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n}\n\nfunction findStructural(sortedEntries: SortedEntry[], parent: Translator | undefined): Translator | undefined {\n for (const entry of structuralCache) {\n if (entry.parent !== parent || entry.sortedEntries.length !== sortedEntries.length) continue;\n if (entry.sortedEntries.every(([k, v], i) => sortedEntries[i][0] === k && sortedEntries[i][1] === v)) {\n return entry.translator;\n }\n }\n return undefined;\n}\n\nexport function getCachedTranslator<T extends TranslationsMap>(\n translations: T,\n parent: Translator | undefined,\n): Translator<T> {\n // Fast path: same object reference\n const cached = getFromWeakMap(translations, parent);\n if (cached) return cached as Translator<T>;\n\n // Slow path: same locale keys and value references, different wrapper object\n const sortedEntries = toSortedEntries(translations);\n const structural = findStructural(sortedEntries, parent);\n if (structural) {\n // Backfill WeakMap so subsequent renders with the same new-object are O(1)\n setInWeakMap(translations, parent, structural);\n return structural as Translator<T>;\n }\n\n const translator = new Translator(translations, parent);\n setInWeakMap(translations, parent, translator);\n structuralCache.unshift({ sortedEntries, parent, translator });\n if (structuralCache.length > MAX_STRUCTURAL_ENTRIES) structuralCache.pop();\n return translator;\n}\n\n/** Exported only for tests — do not use in application code. */\nexport const _structuralCacheSize = (): number => structuralCache.length;\n","import type { Locale, TranslationEntry } from \"@let-value/translate\";\nimport { createElement, Fragment, type ReactElement, type ReactNode, Suspense, use, useId, useMemo } from \"react\";\nimport { type DehydrationEntry, dehydrationContext, localeContext, translatorContext } from \"../context.ts\";\nimport { DEHYDRATION_ATTRIBUTE, dehydrate, isAsyncLocale, localeKeys, readPayload } from \"../dehydration.ts\";\nimport { getCachedTranslator, type TranslationsMap } from \"../translatorCache.ts\";\n\nexport interface TranslationsProviderProps {\n translations?: Partial<Record<Locale, TranslationEntry>>;\n children?: ReactNode;\n}\n\nconst EMPTY_TRANSLATIONS: TranslationsMap = {};\n\ninterface DehydrationScriptProps {\n entry: DehydrationEntry;\n locale: Locale;\n}\n\n/**\n * Inlines the catalog for the active locale as a `<script type=\"application/json\">`.\n *\n * Renders no children, which is the whole point: the provider wraps it in a\n * Suspense boundary that contains nothing of the app's, so awaiting the catalog\n * here can never intercept a fallback the app placed for its own content. The\n * app's boundaries — inside the provider or outside it — behave exactly as\n * written, and `useTranslations` still suspends where it always did.\n */\nfunction DehydrationScript({ entry, locale }: DehydrationScriptProps) {\n // During hydration the payload is already in the document; re-render it\n // verbatim rather than re-encoding an equivalent one, and never call the\n // loader for a catalog the server already sent.\n let text = readPayload(entry, locale);\n\n if (!text) {\n const resource = entry.translator.fetchLocale(locale as never);\n if (resource instanceof Promise) {\n use(resource);\n }\n text = dehydrate(entry.keys, locale, entry.translator.dehydrate(locale));\n }\n\n if (!text) return null;\n\n return createElement(\"script\", {\n type: \"application/json\",\n [DEHYDRATION_ATTRIBUTE]: entry.id,\n dangerouslySetInnerHTML: { __html: text },\n });\n}\n\nexport function TranslationsProvider({\n translations = EMPTY_TRANSLATIONS,\n children,\n}: TranslationsProviderProps): ReactElement {\n const id = useId();\n const parent = use(translatorContext);\n const parentEntry = use(dehydrationContext);\n const translator = getCachedTranslator(translations, parent);\n const locale = use(localeContext);\n\n const entry = useMemo<DehydrationEntry>(\n () => ({ id, translator, keys: localeKeys(translations), parent: parentEntry }),\n [id, translator, translations, parentEntry],\n );\n\n const provide = (inner: ReactNode) =>\n createElement(translatorContext.Provider, { value: translator }, inner) as ReactElement;\n\n if (!isAsyncLocale(translations, locale)) {\n return provide(children);\n }\n\n return provide(\n createElement(\n dehydrationContext.Provider,\n { value: entry },\n createElement(\n Fragment,\n null,\n createElement(\n Suspense,\n { fallback: null },\n createElement(DehydrationScript, { entry, locale: locale as Locale }),\n ),\n children,\n ),\n ),\n );\n}\n","import type { Locale } from \"@let-value/translate\";\nimport { use } from \"react\";\nimport { localeContext } from \"../context.ts\";\n\nexport function useLocale(): Locale | undefined {\n return use(localeContext);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/context.ts","../src/components/LocaleProvider.ts","../src/dehydration.ts","../src/hooks/useTranslations.ts","../src/utils.ts","../src/components/Message.ts","../src/components/Plural.ts","../src/translatorCache.ts","../src/components/TranslationsProvider.ts","../src/hooks/useLocale.ts"],"sourcesContent":["import type { Locale, Translator } from \"@let-value/translate\";\nimport { createContext } from \"react\";\n\ntype AnyTranslator = Translator<any>;\n\n/**\n * A provider's link in the dehydration chain, walked from `useTranslations` so a\n * consumer can seed itself — and every translator it merges against — from the\n * server payload before it would otherwise suspend.\n *\n * Only providers with a lazily-loaded catalog add a link; sync ones are simply\n * skipped, so a child links straight to its nearest lazy ancestor.\n */\nexport interface DehydrationEntry {\n /** `useId()` of the provider, matching the inlined script's attribute. */\n id: string;\n translator: AnyTranslator;\n /** Sorted locale keys of the provider's `translations` prop. */\n keys: string[];\n parent: DehydrationEntry | undefined;\n}\n\nexport const localeContext = createContext<Locale | undefined>(undefined);\nexport const translatorContext = createContext<AnyTranslator | undefined>(undefined);\nexport const dehydrationContext = createContext<DehydrationEntry | undefined>(undefined);\n","import type { Locale } from \"@let-value/translate\";\nimport { createElement, type ReactNode } from \"react\";\nimport { localeContext } from \"../context.ts\";\n\nexport interface LocaleProviderProps {\n locale: Locale;\n children?: ReactNode;\n}\n\nexport function LocaleProvider({ locale, children }: LocaleProviderProps) {\n return createElement(localeContext.Provider, { value: locale }, children);\n}\n","import type { Locale, Translator } from \"@let-value/translate\";\nimport type { GetTextTranslations } from \"gettext-parser\";\nimport type { DehydrationEntry } from \"./context.ts\";\nimport type { TranslationsMap } from \"./translatorCache.ts\";\n\n/**\n * Attribute that marks an inlined catalog. The value is the `useId()` of the\n * `TranslationsProvider` that emitted it — React guarantees that id is identical\n * between the server render and the hydration render of the same tree position.\n */\nexport const DEHYDRATION_ATTRIBUTE = \"data-translations\";\n\ninterface DehydratedPayload {\n /** Sorted locale keys of the provider's `translations` prop. */\n k: string[];\n /** Locale the catalog was resolved for. */\n l: string;\n /** The catalog itself. */\n c: GetTextTranslations;\n}\n\n/** Locale keys of a `translations` map, sorted so server and client agree. */\nexport function localeKeys(translations: TranslationsMap): string[] {\n return Object.keys(translations).sort();\n}\n\n/** True when resolving `locale` from `translations` requires awaiting a loader. */\nexport function isAsyncLocale(translations: TranslationsMap, locale: Locale | undefined): boolean {\n if (!locale) return false;\n const entry = translations[locale];\n if (!entry) return false;\n return typeof entry === \"function\" || \"then\" in entry;\n}\n\n/**\n * `</script` is the only sequence that can terminate the script element early;\n * escaping `<` as a JSON unicode escape keeps the text byte-identical to what\n * `textContent` yields on the client, so hydration matches exactly.\n */\nfunction encode(payload: DehydratedPayload): string {\n return JSON.stringify(payload).replaceAll(\"<\", \"\\\\u003c\");\n}\n\nexport function dehydrate(\n keys: string[],\n locale: Locale,\n catalog: GetTextTranslations | undefined,\n): string | undefined {\n if (!catalog) return undefined;\n return encode({ k: keys, l: locale, c: catalog });\n}\n\n/** Locales already seeded from the document, so a hit costs one DOM query. */\nconst seeded = new WeakMap<Translator, Set<string>>();\n\nfunction markSeeded(translator: Translator, locale: Locale): void {\n let locales = seeded.get(translator);\n if (!locales) {\n locales = new Set();\n seeded.set(translator, locales);\n }\n locales.add(locale);\n}\n\nconst warned = new Set<string>();\n\n/**\n * Two React roots on one page share the `useId` sequence unless they were given\n * distinct `identifierPrefix` values, so their providers can emit scripts under\n * the same id. There is no way to tell from render which one belongs to the tree\n * currently hydrating, so refuse to guess: seeding is skipped and the catalog\n * loads lazily, exactly as it did before this mechanism existed.\n */\nfunction isAmbiguous(id: string, count: number): boolean {\n if (count <= 1) return false;\n if (!warned.has(id)) {\n warned.add(id);\n console.warn(\n `Found ${count} inlined translation catalogs for id \"${id}\". ` +\n \"Give each React root a distinct `identifierPrefix` so their ids cannot collide. \" +\n \"Falling back to loading the catalog.\",\n );\n }\n return true;\n}\n\n/**\n * Seed one provider's translator from the catalog the server inlined for it.\n *\n * Returns the raw payload text when one was applied, so the emitter can render an\n * identical `<script>` back and keep hydration byte-for-byte stable.\n *\n * `useId` is stable across a server render and its hydration, but a provider\n * mounted later by client-side navigation gets a fresh id that could collide with\n * an id from the server tree. The locale keys in the payload guard against that:\n * a mismatch is treated as a miss, never as a match.\n */\nexport function readPayload(entry: DehydrationEntry, locale: Locale): string | undefined {\n if (typeof document === \"undefined\") return undefined;\n if (seeded.get(entry.translator)?.has(locale)) return undefined;\n\n const scripts = document.querySelectorAll(`script[${DEHYDRATION_ATTRIBUTE}=\"${entry.id}\"]`);\n if (isAmbiguous(entry.id, scripts.length)) return undefined;\n\n const text = scripts[0]?.textContent;\n if (!text) return undefined;\n\n let payload: DehydratedPayload;\n try {\n payload = JSON.parse(text) as DehydratedPayload;\n } catch {\n return undefined;\n }\n\n if (payload.l !== locale) return undefined;\n if (payload.k.length !== entry.keys.length || payload.k.some((key, index) => key !== entry.keys[index])) {\n return undefined;\n }\n\n entry.translator.prime(locale, payload.c);\n markSeeded(entry.translator, locale);\n return text;\n}\n\n/**\n * Seed a whole provider chain, outermost first — a child's `getLocale` merges\n * against its parent, so the parent has to be resolved by the time the child is.\n *\n * Called from `useTranslations` before it touches `fetchLocale`, which is what\n * lets the app put its Suspense boundary anywhere: the consumer seeds itself at\n * the exact moment it renders, whether that is with the provider or inside a\n * boundary that hydrates much later.\n */\nexport function seedFromDocument(entry: DehydrationEntry | undefined, locale: Locale): void {\n if (!entry) return;\n seedFromDocument(entry.parent, locale);\n readPayload(entry, locale);\n}\n","import type { Locale, LocaleTranslator } from \"@let-value/translate\";\nimport { use } from \"react\";\n\nimport { dehydrationContext, localeContext, translatorContext } from \"../context.ts\";\nimport { seedFromDocument } from \"../dehydration.ts\";\n\n/** @deprecated replace with `use` from react */\nfunction getPromiseState(promise: any) {\n switch (promise.status) {\n case \"pending\":\n return { status: \"pending\" };\n case \"fulfilled\":\n return { status: \"fulfilled\", value: promise.value };\n case \"rejected\":\n return { status: \"rejected\", reason: promise.reason };\n default: {\n promise.status = \"pending\";\n promise.then((value: unknown) => {\n promise.status = \"fulfilled\";\n promise.value = value;\n });\n promise.catch((reason: unknown) => {\n promise.status = \"rejected\";\n promise.reason = reason;\n });\n return getPromiseState(promise);\n }\n }\n}\n\nexport function useTranslations(locale?: Locale): LocaleTranslator {\n const requestedLocale = locale ?? use(localeContext) ?? (\"unknown\" as never);\n const translator = use(translatorContext);\n const dehydrated = use(dehydrationContext);\n if (!translator) {\n throw new Error(\"TranslationsProvider is missing\");\n }\n\n // Seed before `fetchLocale`, so a catalog the server already inlined never\n // starts its loader — the chunk is not fetched and this render does not\n // suspend, which is what lets React keep the server markup.\n seedFromDocument(dehydrated, requestedLocale);\n\n const resource = translator.fetchLocale(requestedLocale);\n if (!(resource instanceof Promise)) {\n return resource;\n }\n\n const state = getPromiseState(resource);\n if (state.status === \"pending\") {\n throw resource;\n }\n if (state.status === \"rejected\") {\n throw state.reason;\n }\n if (state.status === \"fulfilled\") {\n return state.value;\n }\n\n return use(resource);\n}\n","import { Children, Fragment, isValidElement, type PropsWithChildren, type ReactNode } from \"react\";\n\nexport function buildTemplateFromChildren(children: ReactNode): {\n strings: string[];\n values: ReactNode[];\n} {\n function flatten(nodes: ReactNode): ReactNode[] {\n const result: ReactNode[] = [];\n Children.forEach(nodes, (child) => {\n if (isValidElement(child) && child.type === Fragment) {\n result.push(...flatten((child.props as PropsWithChildren).children));\n } else {\n result.push(child);\n }\n });\n return result;\n }\n\n const array = flatten(children);\n const strings: string[] = [\"\"];\n const values: ReactNode[] = [];\n let expectValue = false;\n\n array.forEach((child) => {\n if (typeof child === \"string\") {\n if (expectValue) {\n values.push(child);\n strings.push(\"\");\n expectValue = false;\n } else {\n strings[strings.length - 1] += child;\n expectValue = true;\n }\n } else if (typeof child === \"number\" || child != null) {\n values.push(child as ReactNode);\n strings.push(\"\");\n expectValue = false;\n }\n });\n\n return { strings, values };\n}\n","import { message } from \"@let-value/translate\";\nimport { createElement, Fragment, type ReactNode } from \"react\";\n\nimport { useTranslations } from \"../hooks/useTranslations.ts\";\nimport { buildTemplateFromChildren } from \"../utils.ts\";\n\nexport interface MessageProps {\n context?: string;\n children: ReactNode;\n}\n\nexport function Message({ context, children }: MessageProps) {\n const translator = useTranslations();\n const { strings, values } = buildTemplateFromChildren(children);\n\n if (values.length === 0) {\n const input = strings.join(\"\");\n if (context) {\n return translator.context(context as \"\").message(input as never);\n }\n return translator.message(input as never);\n }\n\n const tokens = values.map((_, i) => `\\u0000${i}\\u0000`);\n const built = message(strings as unknown as TemplateStringsArray, ...tokens);\n const translated = context\n ? translator.translate({ context: context as \"\", id: built })\n : translator.translate(built);\n\n // oxlint-disable-next-line no-control-regex -- using null separators\n const parts = translated.split(/\\u0000(\\d+)\\u0000/);\n const result: ReactNode[] = [];\n for (let i = 0; i < parts.length; i += 2) {\n result.push(parts[i]);\n const idx = parts[i + 1];\n if (idx !== undefined) {\n result.push(values[Number(idx)]);\n }\n }\n\n return createElement(Fragment, null, ...result);\n}\n","import { message, plural } from \"@let-value/translate\";\nimport { createElement, Fragment, type ReactNode } from \"react\";\n\nimport { useTranslations } from \"../hooks/useTranslations.ts\";\nimport { buildTemplateFromChildren } from \"../utils.ts\";\n\nexport interface PluralProps {\n number: number;\n forms: readonly ReactNode[];\n context?: string;\n}\n\nexport function Plural({ number, forms, context }: PluralProps) {\n const translator = useTranslations();\n\n const built = forms.map((child, i) => {\n const { strings, values } = buildTemplateFromChildren(child);\n const tokens = values.map((_, j) => `\\u0000${i}-${j}\\u0000`);\n return { message: message(strings as unknown as TemplateStringsArray, ...tokens), values };\n });\n\n const messages = built.map((b) => b.message);\n const input = plural(...messages, number);\n\n const translated = context\n ? translator.translate({ context: context as \"\", id: input })\n : translator.translate(input);\n\n // oxlint-disable-next-line no-control-regex -- using null separators\n const parts = translated.split(/\\u0000(\\d+)-(\\d+)\\u0000/);\n const result: ReactNode[] = [];\n for (let i = 0; i < parts.length;) {\n result.push(parts[i]);\n if (i + 2 < parts.length) {\n const formIndex = Number(parts[i + 1]);\n const valueIndex = Number(parts[i + 2]);\n result.push(built[formIndex].values[valueIndex]);\n }\n i += 3;\n }\n\n return createElement(Fragment, null, ...result);\n}\n","import type { Locale, TranslationEntry } from \"@let-value/translate\";\nimport { Translator } from \"@let-value/translate\";\n\nexport type TranslationsMap = Partial<Record<Locale, TranslationEntry>>;\n\n// Fast path: WeakMap keyed on the translations object reference.\n// GC-safe — entries are collected when the object is no longer reachable.\nconst cacheNoParent = new WeakMap<object, Translator>();\nconst cacheWithParent = new WeakMap<object, WeakMap<Translator, Translator>>();\n\nfunction getFromWeakMap(translations: TranslationsMap, parent: Translator | undefined): Translator | undefined {\n if (!parent) return cacheNoParent.get(translations);\n return cacheWithParent.get(translations)?.get(parent);\n}\n\nfunction setInWeakMap(translations: TranslationsMap, parent: Translator | undefined, translator: Translator): void {\n if (!parent) {\n cacheNoParent.set(translations, translator);\n return;\n }\n let byParent = cacheWithParent.get(translations);\n if (!byParent) {\n byParent = new WeakMap();\n cacheWithParent.set(translations, byParent);\n }\n byParent.set(parent, translator);\n}\n\n// Slow path: bounded structural cache for dynamically-created translations objects\n// (e.g. `translations={{ en: data }}` — new wrapper object, stable value references).\n// Bounded to cap memory usage in infinite-render scenarios.\nconst MAX_STRUCTURAL_ENTRIES = 32;\n\ntype SortedEntry = [Locale, TranslationEntry];\n\ninterface StructuralEntry {\n sortedEntries: SortedEntry[];\n parent: Translator | undefined;\n translator: Translator;\n}\n\nconst structuralCache: StructuralEntry[] = [];\n\nfunction toSortedEntries(translations: TranslationsMap): SortedEntry[] {\n return (Object.entries(translations) as SortedEntry[]).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n}\n\nfunction findStructural(sortedEntries: SortedEntry[], parent: Translator | undefined): Translator | undefined {\n for (const entry of structuralCache) {\n if (entry.parent !== parent || entry.sortedEntries.length !== sortedEntries.length) continue;\n if (entry.sortedEntries.every(([k, v], i) => sortedEntries[i][0] === k && sortedEntries[i][1] === v)) {\n return entry.translator;\n }\n }\n return undefined;\n}\n\nexport function getCachedTranslator<T extends TranslationsMap>(\n translations: T,\n parent: Translator | undefined,\n): Translator<T> {\n // Fast path: same object reference\n const cached = getFromWeakMap(translations, parent);\n if (cached) return cached as Translator<T>;\n\n // Slow path: same locale keys and value references, different wrapper object\n const sortedEntries = toSortedEntries(translations);\n const structural = findStructural(sortedEntries, parent);\n if (structural) {\n // Backfill WeakMap so subsequent renders with the same new-object are O(1)\n setInWeakMap(translations, parent, structural);\n return structural as Translator<T>;\n }\n\n const translator = new Translator(translations, parent);\n setInWeakMap(translations, parent, translator);\n structuralCache.unshift({ sortedEntries, parent, translator });\n if (structuralCache.length > MAX_STRUCTURAL_ENTRIES) structuralCache.pop();\n return translator;\n}\n\n/** Exported only for tests — do not use in application code. */\nexport const _structuralCacheSize = (): number => structuralCache.length;\n","import type { Locale, TranslationEntry } from \"@let-value/translate\";\nimport { createElement, Fragment, type ReactElement, type ReactNode, Suspense, use, useId, useMemo } from \"react\";\nimport { type DehydrationEntry, dehydrationContext, localeContext, translatorContext } from \"../context.ts\";\nimport { DEHYDRATION_ATTRIBUTE, dehydrate, isAsyncLocale, localeKeys, readPayload } from \"../dehydration.ts\";\nimport { getCachedTranslator, type TranslationsMap } from \"../translatorCache.ts\";\n\nexport interface TranslationsProviderProps {\n translations?: Partial<Record<Locale, TranslationEntry>>;\n children?: ReactNode;\n}\n\nconst EMPTY_TRANSLATIONS: TranslationsMap = {};\n\ninterface DehydrationScriptProps {\n entry: DehydrationEntry;\n locale: Locale;\n}\n\n/**\n * Inlines the catalog for the active locale as a `<script type=\"application/json\">`.\n *\n * Renders no children, which is the whole point: the provider wraps it in a\n * Suspense boundary that contains nothing of the app's, so awaiting the catalog\n * here can never intercept a fallback the app placed for its own content. The\n * app's boundaries — inside the provider or outside it — behave exactly as\n * written, and `useTranslations` still suspends where it always did.\n */\nfunction DehydrationScript({ entry, locale }: DehydrationScriptProps) {\n // During hydration the payload is already in the document; re-render it\n // verbatim rather than re-encoding an equivalent one, and never call the\n // loader for a catalog the server already sent.\n let text = readPayload(entry, locale);\n\n if (!text) {\n const resource = entry.translator.fetchLocale(locale as never);\n if (resource instanceof Promise) {\n use(resource);\n }\n text = dehydrate(entry.keys, locale, entry.translator.dehydrate(locale));\n }\n\n if (!text) return null;\n\n return createElement(\"script\", {\n type: \"application/json\",\n [DEHYDRATION_ATTRIBUTE]: entry.id,\n dangerouslySetInnerHTML: { __html: text },\n });\n}\n\nexport function TranslationsProvider({\n translations = EMPTY_TRANSLATIONS,\n children,\n}: TranslationsProviderProps): ReactElement {\n const id = useId();\n const parent = use(translatorContext);\n const parentEntry = use(dehydrationContext);\n const translator = getCachedTranslator(translations, parent);\n const locale = use(localeContext);\n\n const entry = useMemo<DehydrationEntry>(\n () => ({ id, translator, keys: localeKeys(translations), parent: parentEntry }),\n [id, translator, translations, parentEntry],\n );\n\n const provide = (inner: ReactNode) =>\n createElement(translatorContext.Provider, { value: translator }, inner) as ReactElement;\n\n if (!isAsyncLocale(translations, locale)) {\n return provide(children);\n }\n\n return provide(\n createElement(\n dehydrationContext.Provider,\n { value: entry },\n createElement(\n Fragment,\n null,\n createElement(\n Suspense,\n { fallback: null },\n createElement(DehydrationScript, { entry, locale: locale as Locale }),\n ),\n children,\n ),\n ),\n );\n}\n","import type { Locale } from \"@let-value/translate\";\nimport { use } from \"react\";\nimport { localeContext } from \"../context.ts\";\n\nexport function useLocale(): Locale | undefined {\n return use(localeContext);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@let-value/translate-react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/let-value/translate"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"./package.json": "./package.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@let-value/translate": "1.2.
|
|
24
|
+
"@let-value/translate": "1.2.5"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/gettext-parser": "9.0.0",
|