Skip to content

feat: allow GlobalConfig.lang to be a function#1490

Open
canseyran wants to merge 5 commits into
open-circle:mainfrom
canseyran:feat-global-lang-callback
Open

feat: allow GlobalConfig.lang to be a function#1490
canseyran wants to merge 5 commits into
open-circle:mainfrom
canseyran:feat-global-lang-callback

Conversation

@canseyran

@canseyran canseyran commented Jun 4, 2026

Copy link
Copy Markdown

Motivation

Valibot currently supports setting, deleting and overwriting the global config. On server environments where messages may need to be translated depending on the user language, we need to include the language everytime we call a parse function. Also some frameworks like SvelteKit might internally call those functions, not allowing us to pass the user language.

Solution

Allow the lang property in GlobalConfig to also accept a function, which resolves when calling getGlobalConfig. By passing a function we can get the request scoped language and it also would make integrating libraries like ParaglideJS easier, since they uses AsyncLocalStorage on the server for storing/retrieving the locale.

import * as v from "valibot";
import { getLocale } from "$lib/paraglide/runtime";

v.setGlobalConfig({ lang: getLocale });

Summary by cubic

Allow GlobalConfig.lang to be a function for request-scoped language resolution on the server. This enables dynamic locales in frameworks that can’t pass lang per call.

  • New Features
    • GlobalConfig.lang accepts a string or () => string.
    • getGlobalConfig invokes the callback on each call; local lang still overrides.
    • Updated types and website docs (API GlobalConfig properties, i18n guide with a callback example).
    • Tests cover callback resolution, per-call invocation, and local override.

Written for commit 90aae3d. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

@canseyran is attempting to deploy a commit to the Open Circle Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. enhancement New feature or request labels Jun 4, 2026
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Complex PR? Review this PR in Change Stack to move by importance, not file order.

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db471980-fef2-462e-ab9e-5fdc24d91ac9

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea2e78 and 90aae3d.

📒 Files selected for processing (2)
  • library/src/storages/globalConfig/globalConfig.test.ts
  • website/src/routes/guides/(advanced)/internationalization/index.mdx
✅ Files skipped from review due to trivial changes (1)
  • website/src/routes/guides/(advanced)/internationalization/index.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • library/src/storages/globalConfig/globalConfig.test.ts

Walkthrough

This PR extends GlobalConfig to support dynamic language configuration by allowing lang to be either a static string or a function returning a string. The GlobalConfig type definition was updated to omit lang from the base Config and add it back as an optional field with the union type. The getGlobalConfig function's resolution logic now invokes callable lang values on each access and respects local overrides. Test coverage was added to verify function resolution, non-caching behavior across calls, and local override semantics. Documentation was updated in both the API schema and the user guide to reflect the new capability.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature: allowing GlobalConfig.lang to be a function for runtime language resolution.
Description check ✅ Passed The description is well-related to the changeset, explaining the motivation for server-scoped languages, the solution via callable lang functions, and providing concrete usage examples.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
library/src/storages/globalConfig/globalConfig.ts (1)

6-11: ⚡ Quick win

Use interface for GlobalConfig to match repository TypeScript conventions.

This object-shape declaration should be an interface instead of a type alias intersection.

♻️ Proposed change
-export type GlobalConfig = Omit<Config<never>, 'lang' | 'message'> & {
+export interface GlobalConfig extends Omit<Config<never>, 'lang' | 'message'> {
   /**
    * The selected language or a function that returns it.
    */
   readonly lang?: string | (() => string);
-};
+}

As per coding guidelines, **/*.{ts,tsx}: Prefer interface over type for defining object shapes in TypeScript.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/src/storages/globalConfig/globalConfig.ts` around lines 6 - 11,
Replace the exported type alias GlobalConfig with an exported interface named
GlobalConfig that extends Omit<Config<never>, 'lang' | 'message'> and declares
the same readonly lang?: string | (() => string); so the shape uses an interface
(keep the export, the generic base Config reference, and the readonly lang
field) to follow the repository convention preferring interfaces for object
shapes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@library/src/storages/globalConfig/globalConfig.test.ts`:
- Around line 44-50: The test relies on prior test state because it expects
customConfig to be present but only sets lang; make it self-contained by
explicitly applying customConfig before setting lang: call
setGlobalConfig(customConfig) (or reset/apply initialConfig then set
customConfig) at the start of the test, then call setGlobalConfig({ lang: () =>
'fr' }) and assert getGlobalConfig() equals { ...initialConfig, ...customConfig,
lang: 'fr' } so the expectation no longer depends on earlier tests; reference
setGlobalConfig, getGlobalConfig, initialConfig and customConfig to locate and
update this test.

In `@website/src/routes/guides/`(advanced)/internationalization/index.mdx:
- Around line 53-54: The example uses currentLanguage in the callback passed to
v.setGlobalConfig but never declares it; update the snippet to define and
initialize currentLanguage (e.g., const/let currentLanguage = 'en' or show how
it’s derived from context) before calling v.setGlobalConfig({ lang: () =>
currentLanguage }), and if relevant show how to update currentLanguage when the
user changes language so the callback returns the correct value.

---

Nitpick comments:
In `@library/src/storages/globalConfig/globalConfig.ts`:
- Around line 6-11: Replace the exported type alias GlobalConfig with an
exported interface named GlobalConfig that extends Omit<Config<never>, 'lang' |
'message'> and declares the same readonly lang?: string | (() => string); so the
shape uses an interface (keep the export, the generic base Config reference, and
the readonly lang field) to follow the repository convention preferring
interfaces for object shapes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65d87806-969c-4f70-98e9-23e96f100a24

📥 Commits

Reviewing files that changed from the base of the PR and between e7bcacb and 0ea2e78.

📒 Files selected for processing (4)
  • library/src/storages/globalConfig/globalConfig.test.ts
  • library/src/storages/globalConfig/globalConfig.ts
  • website/src/routes/api/(types)/GlobalConfig/properties.ts
  • website/src/routes/guides/(advanced)/internationalization/index.mdx

Comment thread library/src/storages/globalConfig/globalConfig.test.ts Outdated
Comment thread website/src/routes/guides/(advanced)/internationalization/index.mdx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread website/src/routes/guides/(advanced)/internationalization/index.mdx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="website/src/routes/guides/(advanced)/internationalization/index.mdx">

<violation number="1" location="website/src/routes/guides/(advanced)/internationalization/index.mdx:49">
P2: Primary i18n example uses SvelteKit-specific `$lib` path alias in a generic guide</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


```ts
import * as v from 'valibot';
import { getLocale } from "$lib/paraglide/runtime";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Primary i18n example uses SvelteKit-specific $lib path alias in a generic guide

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website/src/routes/guides/(advanced)/internationalization/index.mdx, line 49:

<comment>Primary i18n example uses SvelteKit-specific `$lib` path alias in a generic guide</comment>

<file context>
@@ -46,12 +46,14 @@ The language used is then selected by the `lang` configuration. You can set it g
 
 ```ts
 import * as v from 'valibot';
+import { getLocale } from "$lib/paraglide/runtime";
+
 
</file context>
Suggested change
import { getLocale } from "$lib/paraglide/runtime";
import { getLocale } from './paraglide/runtime';

@fabian-hiller

Copy link
Copy Markdown
Member

Hey, thank you for creating this PR! Just a quick question. Why is passing a function preferred over calling v.setGlobalConfig when you resolved the language. You can call v.setGlobalConfig multiple times.

If there a good reasons for using a function, I wonder if we should only allow a function for the global config or also for the local config.

@pkg-pr-new

pkg-pr-new Bot commented Jun 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/valibot@1490

commit: 90aae3d

@canseyran

canseyran commented Jun 7, 2026

Copy link
Copy Markdown
Author

Hey, thank you for creating this PR! Just a quick question. Why is passing a function preferred over calling v.setGlobalConfig when you resolved the language. You can call v.setGlobalConfig multiple times.

If there a good reasons for using a function, I wonder if we should only allow a function for the global config or also for the local config.

Thanks for checking out the PR! The issue is that everytime you call v.setGlobalConfig you overwrite the previous language, this is fine for stateful environments like the browser, where you have may only have one language which is tied to the whole application. On stateless environments like the server, calling v.setGlobalConfig multiple times would overwrite the config for all current requests, causing a race condition. So users will very likely end up with a wrong translation, caused by another request with a different user language.

We could solve this by passing a function which resolves when v.getGlobalConfig is called either directly or by calling any parser function like v.safeParse and then get the language from the request context. Something like:

// Needs to be called only once on app init
v.setGlobalConfig({lang: () => getCurrentRequestContext().lang})

You mentioned passing a function for the local config aswell. I am not entirely sure how the configs are resolved within the whole code base, but as far as I can see the local config is resolved immediately when calling the function.

v.safeParse(schema, data, { lang: getLocale()}

v.safeParse(schema, data, { lang: getLocale})

Those two calls would up end up always having the same result, assuming the code snippet runs synchronously. But I might be wrong, if the local config is resolved differently in any part of the library, it might solve a problem, otherwise there is no practical difference or advantage between those two function calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants