next-tongue 0.9.8 → 0.9.10
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/cli/init.js +6 -8
- package/dist/compiler/build.js +7 -4
- package/dist/compiler/validate/a11y.js +5 -1
- package/dist/compiler/validate/catalog.d.ts +5 -0
- package/dist/compiler/validate/catalog.js +21 -2
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.js +14 -0
- package/dist/next/with-tongue.d.ts +3 -0
- package/dist/next/with-tongue.js +3 -0
- package/dist/proxy/index.d.ts +2 -0
- package/dist/proxy/index.js +3 -1
- package/dist/runtime/create-t.d.ts +6 -2
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +32 -1
- package/dist/server/request-locale.d.ts +27 -0
- package/dist/server/request-locale.js +22 -0
- package/package.json +1 -1
package/dist/cli/init.js
CHANGED
|
@@ -83,14 +83,12 @@ export default defineConfig({
|
|
|
83
83
|
path: `${appRoot}/[locale]/page.tsx`,
|
|
84
84
|
kind: 'create',
|
|
85
85
|
content: `import { getT } from 'next-tongue/server';
|
|
86
|
-
import { ensureTongue } from '#tongue/runtime';
|
|
87
86
|
|
|
88
87
|
export default async function Page({
|
|
89
88
|
params,
|
|
90
89
|
}: {
|
|
91
90
|
params: Promise<{ locale: string }>;
|
|
92
91
|
}) {
|
|
93
|
-
ensureTongue();
|
|
94
92
|
const { locale } = await params;
|
|
95
93
|
const t = await getT(locale, 'home');
|
|
96
94
|
return (
|
|
@@ -246,10 +244,10 @@ export const config = {
|
|
|
246
244
|
function rootLocaleLayout() {
|
|
247
245
|
return `import { notFound } from 'next/navigation';
|
|
248
246
|
import { TongueProvider } from 'next-tongue/client';
|
|
249
|
-
import { getDir, isLocale
|
|
250
|
-
import {
|
|
247
|
+
import { getDir, isLocale } from 'next-tongue/server';
|
|
248
|
+
import { localeParams } from '#tongue/runtime';
|
|
251
249
|
|
|
252
|
-
ensureTongue()
|
|
250
|
+
// Importing #tongue/runtime auto-runs ensureTongue() (withTongue apps).
|
|
253
251
|
export const generateStaticParams = localeParams;
|
|
254
252
|
|
|
255
253
|
export default async function LocaleLayout({
|
|
@@ -278,10 +276,10 @@ export default async function LocaleLayout({
|
|
|
278
276
|
function nestedLocaleLayout() {
|
|
279
277
|
return `import { notFound } from 'next/navigation';
|
|
280
278
|
import { TongueProvider } from 'next-tongue/client';
|
|
281
|
-
import { isLocale
|
|
282
|
-
import {
|
|
279
|
+
import { isLocale } from 'next-tongue/server';
|
|
280
|
+
import { localeParams } from '#tongue/runtime';
|
|
283
281
|
|
|
284
|
-
ensureTongue()
|
|
282
|
+
// Importing #tongue/runtime auto-runs ensureTongue() (withTongue apps).
|
|
285
283
|
export const generateStaticParams = localeParams;
|
|
286
284
|
|
|
287
285
|
export default async function LocaleLayout({
|
package/dist/compiler/build.js
CHANGED
|
@@ -113,8 +113,8 @@ export async function buildMessages(options) {
|
|
|
113
113
|
const defaultMsgRel = config.messages.replaceAll('{locale}', config.defaultLocale);
|
|
114
114
|
const emitLocales = localesForEmit(config);
|
|
115
115
|
// Content hash for idempotency (bump CODEGEN_VERSION when emit format changes)
|
|
116
|
-
// Bump when emit shape changes (
|
|
117
|
-
const CODEGEN_VERSION =
|
|
116
|
+
// Bump when emit shape changes (reserved idents, auto ensureTongue side-effect).
|
|
117
|
+
const CODEGEN_VERSION = 9;
|
|
118
118
|
const hash = createHash('sha256')
|
|
119
119
|
.update(JSON.stringify({
|
|
120
120
|
codegen: CODEGEN_VERSION,
|
|
@@ -337,7 +337,10 @@ export async function buildMessages(options) {
|
|
|
337
337
|
? `export const pseudoLocale = ${JSON.stringify(config.pseudo.locale)};`
|
|
338
338
|
: `export const pseudoLocale = undefined;`, `export function localeParams() {`, ` return locales.map((locale) => ({ locale }));`, `}`, `let ready = false;`, `export function ensureTongue() {`, ` if (ready) return;`, ` __setServerRuntime({`, ` locales: allLocales,`, ` defaultLocale,`, ` timeZone,`, ` rootParams: ${config.routing.rootParams ? 'true' : 'false'},`, ` loadNamespace: async (namespace) => {`, ` const messages = namespaces[namespace];`, ` if (!messages) throw new Error('[next-tongue] Unknown namespace: ' + namespace);`, ` return messages;`, ` },`, ` });`, config.pseudo && config.pseudo.enabled !== false
|
|
339
339
|
? ` configureFormatMessage({ timeZone, pseudo: { locale: ${JSON.stringify(config.pseudo.locale)}, expand: ${config.pseudo.expand} } });`
|
|
340
|
-
: ` configureFormatMessage({ timeZone });`, ` ready = true;`, `}`,
|
|
340
|
+
: ` configureFormatMessage({ timeZone });`, ` ready = true;`, `}`,
|
|
341
|
+
// Side-effect: importing `#tongue/runtime` (or getT's lazy import) wires the
|
|
342
|
+
// server without a per-page ensureTongue() call when using withTongue().
|
|
343
|
+
`ensureTongue();`, ``);
|
|
341
344
|
await writeFile(join(outDir, 'runtime.js'), runtimeLines.join('\n'), 'utf8');
|
|
342
345
|
await writeFile(join(outDir, 'runtime.d.ts'), [
|
|
343
346
|
`/** Generated by next-tongue — do not edit. */`,
|
|
@@ -407,7 +410,7 @@ export async function buildMessages(options) {
|
|
|
407
410
|
await writeFile(join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
408
411
|
await writeFile(join(outDir, '.hash'), hash, 'utf8');
|
|
409
412
|
// package.json imports helper note — apps map "#tongue/*" themselves / via withTongue
|
|
410
|
-
await writeFile(join(outDir, 'README.md'), 'Generated by next-tongue. Do not edit. Map `#tongue/*` → this directory.\
|
|
413
|
+
await writeFile(join(outDir, 'README.md'), 'Generated by next-tongue. Do not edit. Map `#tongue/*` → this directory.\nImporting `#tongue/runtime` (or calling getT) auto-runs ensureTongue(); an explicit call is optional.\n', 'utf8');
|
|
411
414
|
await clearOutputDir(publishedOutDir);
|
|
412
415
|
try {
|
|
413
416
|
await rename(outDir, publishedOutDir);
|
|
@@ -75,6 +75,8 @@ function hasLangHint(src) {
|
|
|
75
75
|
/<html[^>]*\blang=["']/.test(src) ||
|
|
76
76
|
/\blang=\{locale\}/.test(src) ||
|
|
77
77
|
/document\.documentElement\.lang/.test(src) ||
|
|
78
|
+
/setAttribute\(\s*['"]lang['"]/.test(src) ||
|
|
79
|
+
/\bHtmlLang\b/.test(src) ||
|
|
78
80
|
/\.lang\s*=/.test(src));
|
|
79
81
|
}
|
|
80
82
|
function hasDirHint(src) {
|
|
@@ -82,7 +84,9 @@ function hasDirHint(src) {
|
|
|
82
84
|
/<html[^>]*\bdir=["']/.test(src) ||
|
|
83
85
|
/\bdir=\{getDir\(/.test(src) ||
|
|
84
86
|
/\bdir=\{.*getDir/.test(src) ||
|
|
85
|
-
/document\.documentElement\.dir/.test(src)
|
|
87
|
+
/document\.documentElement\.dir/.test(src) ||
|
|
88
|
+
/setAttribute\(\s*['"]dir['"]/.test(src) ||
|
|
89
|
+
/\bHtmlLang\b/.test(src));
|
|
86
90
|
}
|
|
87
91
|
async function findLocaleLayouts(cwd) {
|
|
88
92
|
return findLayoutsUnder(cwd, ['app/[locale]', 'src/app/[locale]']);
|
|
@@ -22,3 +22,8 @@ export declare function validateStale(config: ResolvedTongueConfig, cwd: string,
|
|
|
22
22
|
}): Promise<CheckIssue[]>;
|
|
23
23
|
/** Letter + single quote + letter — usually meant as prose apostrophe, not ICU quote. */
|
|
24
24
|
export declare function likelyUnescapedApostrophe(icu: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Apostrophe-in-words is common in fr/it/…; English `don't` floods CI with noise.
|
|
27
|
+
* Only hint for locales that regularly need ICU `''` in prose.
|
|
28
|
+
*/
|
|
29
|
+
export declare function shouldHintApostrophe(locale: string): boolean;
|
|
@@ -69,11 +69,12 @@ export async function validateCatalog(config, cwd, opts) {
|
|
|
69
69
|
});
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
72
|
-
if (
|
|
72
|
+
if (shouldHintApostrophe(config.defaultLocale) &&
|
|
73
|
+
likelyUnescapedApostrophe(source)) {
|
|
73
74
|
issues.push({
|
|
74
75
|
check: 'icu',
|
|
75
76
|
severity: 'warn',
|
|
76
|
-
message: `Key "${fullKey}" looks like a natural-language apostrophe
|
|
77
|
+
message: `Key "${fullKey}" looks like a natural-language apostrophe. In ICU, double it: "l''été" → l'été`,
|
|
77
78
|
key: fullKey,
|
|
78
79
|
locale: config.defaultLocale,
|
|
79
80
|
file,
|
|
@@ -142,6 +143,17 @@ export async function validateCatalog(config, cwd, opts) {
|
|
|
142
143
|
});
|
|
143
144
|
continue;
|
|
144
145
|
}
|
|
146
|
+
if (shouldHintApostrophe(locale) &&
|
|
147
|
+
likelyUnescapedApostrophe(resolved)) {
|
|
148
|
+
issues.push({
|
|
149
|
+
check: 'icu',
|
|
150
|
+
severity: 'warn',
|
|
151
|
+
message: `Key "${fullKey}" looks like a natural-language apostrophe. In ICU, double it: "l''été" → l'été`,
|
|
152
|
+
key: fullKey,
|
|
153
|
+
locale,
|
|
154
|
+
file: localeFile,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
145
157
|
const mismatchSev = severityFor(config.strict.mismatch);
|
|
146
158
|
if (mismatchSev) {
|
|
147
159
|
const otherArgs = collectArgs(otherAst);
|
|
@@ -247,3 +259,10 @@ export function likelyUnescapedApostrophe(icu) {
|
|
|
247
259
|
}
|
|
248
260
|
return false;
|
|
249
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Apostrophe-in-words is common in fr/it/…; English `don't` floods CI with noise.
|
|
264
|
+
* Only hint for locales that regularly need ICU `''` in prose.
|
|
265
|
+
*/
|
|
266
|
+
export function shouldHintApostrophe(locale) {
|
|
267
|
+
return /^(fr|it|ca|oc|ht|lb|rm|ga|gd|mt)(-|$)/i.test(locale);
|
|
268
|
+
}
|
|
@@ -48,6 +48,7 @@ export declare function createNavigation(config: NavigationConfig): {
|
|
|
48
48
|
refresh: () => void;
|
|
49
49
|
};
|
|
50
50
|
usePathname: () => string;
|
|
51
|
+
usePathnameWithoutLocale: () => string;
|
|
51
52
|
getPathname: (args: {
|
|
52
53
|
href: Href;
|
|
53
54
|
locale: Locale;
|
|
@@ -57,3 +58,10 @@ export declare function createNavigation(config: NavigationConfig): {
|
|
|
57
58
|
children: (options: import("./locale-switcher.js").UseLocaleSwitcherResult) => ReactNode;
|
|
58
59
|
}) => import("react").JSX.Element;
|
|
59
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Strip a leading locale segment from `next/navigation`'s pathname.
|
|
63
|
+
* Prefer `createNavigation(…).usePathname` / `usePathnameWithoutLocale` when
|
|
64
|
+
* you already use tongue navigation; this unbound helper is for apps that keep
|
|
65
|
+
* plain `next/link` + proxy for as-needed paths.
|
|
66
|
+
*/
|
|
67
|
+
export declare function usePathnameWithoutLocale(locales: readonly string[]): string;
|
package/dist/navigation/index.js
CHANGED
|
@@ -77,6 +77,10 @@ export function createNavigation(config) {
|
|
|
77
77
|
const locale = useLocale();
|
|
78
78
|
return toInternalPathname(stripped, locale, bound.pathnames);
|
|
79
79
|
}
|
|
80
|
+
/** Same as `usePathname` when pathnames are unset — locale prefix stripped. */
|
|
81
|
+
function usePathnameWithoutLocale() {
|
|
82
|
+
return stripLocalePrefix(useNextPathname(), bound.locales);
|
|
83
|
+
}
|
|
80
84
|
const { useLocaleSwitcher, LocaleSwitcher } = createLocaleSwitcher(config);
|
|
81
85
|
return {
|
|
82
86
|
Link,
|
|
@@ -84,8 +88,18 @@ export function createNavigation(config) {
|
|
|
84
88
|
permanentRedirect,
|
|
85
89
|
useRouter,
|
|
86
90
|
usePathname,
|
|
91
|
+
usePathnameWithoutLocale,
|
|
87
92
|
getPathname: (args) => resolve(args.href, args.locale),
|
|
88
93
|
useLocaleSwitcher,
|
|
89
94
|
LocaleSwitcher,
|
|
90
95
|
};
|
|
91
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Strip a leading locale segment from `next/navigation`'s pathname.
|
|
99
|
+
* Prefer `createNavigation(…).usePathname` / `usePathnameWithoutLocale` when
|
|
100
|
+
* you already use tongue navigation; this unbound helper is for apps that keep
|
|
101
|
+
* plain `next/link` + proxy for as-needed paths.
|
|
102
|
+
*/
|
|
103
|
+
export function usePathnameWithoutLocale(locales) {
|
|
104
|
+
return stripLocalePrefix(useNextPathname(), locales);
|
|
105
|
+
}
|
|
@@ -6,5 +6,8 @@ export type WithTongueOptions = {
|
|
|
6
6
|
/**
|
|
7
7
|
* Config wrapper (not a bundler plugin): runs codegen when Next loads config,
|
|
8
8
|
* maps `#tongue/*` → outDir, registers the call-site loader for Turbopack + webpack.
|
|
9
|
+
*
|
|
10
|
+
* Codegen emits `#tongue/runtime` that auto-calls `ensureTongue()` on import;
|
|
11
|
+
* `getT` / `getFormat` also lazy-import that module — no per-page ensureTongue().
|
|
9
12
|
*/
|
|
10
13
|
export declare function withTongue<T extends object>(nextConfig?: T, options?: WithTongueOptions): T;
|
package/dist/next/with-tongue.js
CHANGED
|
@@ -7,6 +7,9 @@ import { startDevWatcher } from './watcher.js';
|
|
|
7
7
|
/**
|
|
8
8
|
* Config wrapper (not a bundler plugin): runs codegen when Next loads config,
|
|
9
9
|
* maps `#tongue/*` → outDir, registers the call-site loader for Turbopack + webpack.
|
|
10
|
+
*
|
|
11
|
+
* Codegen emits `#tongue/runtime` that auto-calls `ensureTongue()` on import;
|
|
12
|
+
* `getT` / `getFormat` also lazy-import that module — no per-page ensureTongue().
|
|
10
13
|
*/
|
|
11
14
|
export function withTongue(nextConfig = {}, options = {}) {
|
|
12
15
|
const cwd = process.cwd();
|
package/dist/proxy/index.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export type CreateProxyOptions = {
|
|
|
48
48
|
resolveLocale?: (req: TongueProxyRequest) => string | undefined | Promise<string | undefined>;
|
|
49
49
|
onLocale?: (locale: string, req: TongueProxyRequest) => void;
|
|
50
50
|
};
|
|
51
|
+
/** Default locale cookie name (`SameSite=Lax`). Use with `requestLocale` in API routes. */
|
|
52
|
+
export declare const LOCALE_COOKIE_NAME = "TONGUE_LOCALE";
|
|
51
53
|
/**
|
|
52
54
|
* Next.js 16 `proxy.ts` handler for locale prefix routing.
|
|
53
55
|
* Returns standard `Response` objects (compatible with Next proxy/middleware).
|
package/dist/proxy/index.js
CHANGED
|
@@ -2,8 +2,10 @@ import { toInternalPathname } from '../navigation/pathname.js';
|
|
|
2
2
|
import { createLocaleMatcher, } from './locale-matcher.js';
|
|
3
3
|
export { createLocaleMatcher, buildLocaleMatcherTables, emitLocaleMatcherModule, localeParentChain, matchTagAgainstTables, } from './locale-matcher.js';
|
|
4
4
|
export { verifyLocalePreference, signLocalePreference, timingSafeEqual, DEFAULT_PREF_COOKIE, } from '../runtime/locale-preference.js';
|
|
5
|
+
/** Default locale cookie name (`SameSite=Lax`). Use with `requestLocale` in API routes. */
|
|
6
|
+
export const LOCALE_COOKIE_NAME = 'TONGUE_LOCALE';
|
|
5
7
|
const DEFAULT_COOKIE = {
|
|
6
|
-
name:
|
|
8
|
+
name: LOCALE_COOKIE_NAME,
|
|
7
9
|
maxAge: 60 * 60 * 24 * 365,
|
|
8
10
|
sameSite: 'lax',
|
|
9
11
|
path: '/',
|
|
@@ -14,8 +14,12 @@ export type TFunction<N extends Namespace = Namespace> = {
|
|
|
14
14
|
<K extends MessageKey<N>>(key: K, ...args: [ArgsFor<N, K>] extends [never] ? [values?: Record<string, never>] : Record<string, never> extends ArgsFor<N, K> ? [values?: ArgsFor<N, K>] : [values: ArgsFor<N, K>]): string;
|
|
15
15
|
rich: <K extends MessageKey<N>>(key: K, ...args: [ArgsFor<N, K>] extends [never] ? [values?: Record<string, never>] : Record<string, never> extends ArgsFor<N, K> ? [values?: ArgsFor<N, K>] : [values: ArgsFor<N, K>]) => ReactNode;
|
|
16
16
|
has: (key: string) => boolean;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Non-literal key from a declared set. `key` is narrowed to `From[number]`
|
|
19
|
+
* when `from` is a `as const` / readonly tuple of known keys.
|
|
20
|
+
*/
|
|
21
|
+
dynamic: <const From extends readonly MessageKey<N>[]>(key: From[number], opts: {
|
|
22
|
+
from: From;
|
|
19
23
|
}, values?: Record<string, unknown>) => string;
|
|
20
24
|
ns: <N2 extends Namespace>(namespace: N2) => TFunction<N2>;
|
|
21
25
|
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -29,11 +29,20 @@ export declare const defaultLocale: {
|
|
|
29
29
|
export type LocaleProps = {
|
|
30
30
|
locale: string;
|
|
31
31
|
};
|
|
32
|
+
/** Map of translators when loading several namespaces in one call. */
|
|
33
|
+
export type MultiT<Ns extends readonly Namespace[]> = {
|
|
34
|
+
[K in Ns[number]]: TFunction<K>;
|
|
35
|
+
};
|
|
32
36
|
/**
|
|
33
37
|
* Explicit locale (always available): `getT(locale, ns)` or `getT(locale)` for `_root`.
|
|
38
|
+
* Multi-ns: `getT(locale, ['home', 'common'])` → `{ home, common }` translators.
|
|
34
39
|
* With `routing.rootParams: true`, `getT(ns)` resolves locale from `next/root-params`
|
|
35
40
|
* when the single argument is a configured namespace (not a locale tag).
|
|
41
|
+
*
|
|
42
|
+
* `withTongue()` + generated `#tongue/runtime` auto-wires messages; an explicit
|
|
43
|
+
* `ensureTongue()` call is optional.
|
|
36
44
|
*/
|
|
45
|
+
export declare function getT<const Ns extends readonly [Namespace, ...Namespace[]]>(locale: Locale | string, namespaces: Ns): Promise<MultiT<Ns>>;
|
|
37
46
|
export declare function getT<N extends Namespace>(locale: Locale | string, namespace: N): Promise<TFunction<N>>;
|
|
38
47
|
export declare function getT(locale: Locale | string): Promise<TFunction<'_root'>>;
|
|
39
48
|
export declare function getFormat(locale?: string, options?: {
|
|
@@ -61,3 +70,5 @@ export type { Format, SanctionedUnit, SimpleUnit, DisplayNameType, } from '../ru
|
|
|
61
70
|
export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
|
|
62
71
|
export { setLocalePreference, } from './locale-preference.js';
|
|
63
72
|
export type { SetLocalePreferenceOptions, CookieStoreLike, } from './locale-preference.js';
|
|
73
|
+
export { requestLocale, LOCALE_COOKIE_NAME, LOCALE_HEADER_NAME, } from './request-locale.js';
|
|
74
|
+
export type { RequestLocaleOptions, RequestLocaleSource, } from './request-locale.js';
|
package/dist/server/index.js
CHANGED
|
@@ -42,6 +42,18 @@ export const defaultLocale = {
|
|
|
42
42
|
};
|
|
43
43
|
export async function getT(localeOrNs, namespace) {
|
|
44
44
|
if (namespace !== undefined) {
|
|
45
|
+
if (Array.isArray(namespace)) {
|
|
46
|
+
if (namespace.length === 0) {
|
|
47
|
+
throw new TongueError('TONGUE_E_CONFIG', 'getT(locale, namespaces) requires a non-empty namespace array');
|
|
48
|
+
}
|
|
49
|
+
const locale = String(localeOrNs);
|
|
50
|
+
const out = {};
|
|
51
|
+
// Load sequentially so a single ensureRuntime runs first; namespaces share locale.
|
|
52
|
+
for (const ns of namespace) {
|
|
53
|
+
out[ns] = await getTWithLocale(locale, ns);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
45
57
|
return getTWithLocale(String(localeOrNs), namespace);
|
|
46
58
|
}
|
|
47
59
|
const single = String(localeOrNs);
|
|
@@ -54,9 +66,10 @@ export async function getT(localeOrNs, namespace) {
|
|
|
54
66
|
return getTWithLocale(single, '_root');
|
|
55
67
|
}
|
|
56
68
|
async function getTWithLocale(locale, ns) {
|
|
69
|
+
await ensureRuntime();
|
|
57
70
|
assertLocale(locale);
|
|
58
71
|
if (!namespaceLoader) {
|
|
59
|
-
throw new TongueError('TONGUE_E_TYPES_MISSING', 'Message modules not loaded.
|
|
72
|
+
throw new TongueError('TONGUE_E_TYPES_MISSING', 'Message modules not loaded. Wrap next.config with withTongue() and run `tongue build && next build` so `#tongue/runtime` exists. (ensureTongue() is optional — getT loads it automatically.)');
|
|
60
73
|
}
|
|
61
74
|
const messages = await namespaceLoader(ns);
|
|
62
75
|
return createT({
|
|
@@ -67,7 +80,24 @@ async function getTWithLocale(locale, ns) {
|
|
|
67
80
|
},
|
|
68
81
|
});
|
|
69
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Load `#tongue/runtime` and call `ensureTongue()` once when the server runtime
|
|
85
|
+
* is not yet wired (withTongue apps — no per-page ensureTongue boilerplate).
|
|
86
|
+
*/
|
|
87
|
+
async function ensureRuntime() {
|
|
88
|
+
if (namespaceLoader)
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
const importer = new Function('s', 'return import(s)');
|
|
92
|
+
const mod = await importer('#tongue/runtime');
|
|
93
|
+
mod.ensureTongue?.();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* fall through — getTWithLocale throws a clear error */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
70
99
|
export async function getFormat(locale, options) {
|
|
100
|
+
await ensureRuntime();
|
|
71
101
|
const resolved = locale ?? (runtimeRootParams ? await resolveRootLocale() : undefined);
|
|
72
102
|
if (resolved == null) {
|
|
73
103
|
throw new TongueError('TONGUE_E_LOCALE', 'getFormat(locale) requires a locale, or routing.rootParams: true');
|
|
@@ -141,6 +171,7 @@ export { generateAlternates, createGenerateAlternates, } from './alternates.js';
|
|
|
141
171
|
export { translateIssues, defaultFormatPath, } from '../runtime/translate-issues.js';
|
|
142
172
|
export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
|
|
143
173
|
export { setLocalePreference, } from './locale-preference.js';
|
|
174
|
+
export { requestLocale, LOCALE_COOKIE_NAME, LOCALE_HEADER_NAME, } from './request-locale.js';
|
|
144
175
|
function assertLocale(locale) {
|
|
145
176
|
if (runtimeLocales.length && !runtimeLocales.includes(locale)) {
|
|
146
177
|
throw new TongueError('TONGUE_E_LOCALE', `Locale "${locale}" is not in configured locales [${runtimeLocales.join(', ')}]`);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Default cookie name set by `createProxy` (`SameSite=Lax`). */
|
|
2
|
+
export declare const LOCALE_COOKIE_NAME = "TONGUE_LOCALE";
|
|
3
|
+
/** Common header for client → API locale (wizard / fetch). */
|
|
4
|
+
export declare const LOCALE_HEADER_NAME = "x-locale";
|
|
5
|
+
export type RequestLocaleSource = {
|
|
6
|
+
headers: {
|
|
7
|
+
get: (name: string) => string | null;
|
|
8
|
+
};
|
|
9
|
+
cookies?: {
|
|
10
|
+
get: (name: string) => {
|
|
11
|
+
value: string;
|
|
12
|
+
} | undefined;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type RequestLocaleOptions = {
|
|
16
|
+
locales: readonly string[];
|
|
17
|
+
defaultLocale: string;
|
|
18
|
+
/** Defaults to `TONGUE_LOCALE`. */
|
|
19
|
+
cookieName?: string;
|
|
20
|
+
/** Defaults to `x-locale`. */
|
|
21
|
+
headerName?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve locale in Route Handlers / Server Actions (no `params.locale`).
|
|
25
|
+
* Order: header → cookie → defaultLocale.
|
|
26
|
+
*/
|
|
27
|
+
export declare function requestLocale(req: RequestLocaleSource, opts: RequestLocaleOptions): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isLocale as isLocaleValue } from '../runtime/locale.js';
|
|
2
|
+
/** Default cookie name set by `createProxy` (`SameSite=Lax`). */
|
|
3
|
+
export const LOCALE_COOKIE_NAME = 'TONGUE_LOCALE';
|
|
4
|
+
/** Common header for client → API locale (wizard / fetch). */
|
|
5
|
+
export const LOCALE_HEADER_NAME = 'x-locale';
|
|
6
|
+
/**
|
|
7
|
+
* Resolve locale in Route Handlers / Server Actions (no `params.locale`).
|
|
8
|
+
* Order: header → cookie → defaultLocale.
|
|
9
|
+
*/
|
|
10
|
+
export function requestLocale(req, opts) {
|
|
11
|
+
const headerName = opts.headerName ?? LOCALE_HEADER_NAME;
|
|
12
|
+
const cookieName = opts.cookieName ?? LOCALE_COOKIE_NAME;
|
|
13
|
+
const fromHeader = req.headers.get(headerName)?.trim();
|
|
14
|
+
if (fromHeader && isLocaleValue(fromHeader, opts.locales)) {
|
|
15
|
+
return fromHeader;
|
|
16
|
+
}
|
|
17
|
+
const fromCookie = req.cookies?.get(cookieName)?.value?.trim();
|
|
18
|
+
if (fromCookie && isLocaleValue(fromCookie, opts.locales)) {
|
|
19
|
+
return fromCookie;
|
|
20
|
+
}
|
|
21
|
+
return opts.defaultLocale;
|
|
22
|
+
}
|