next-tongue 0.9.8 → 0.9.9

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.
@@ -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 (likelyUnescapedApostrophe(source)) {
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 (e.g. French). In ICU, double it: "l''été" → l'été`,
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;
@@ -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
+ }
@@ -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).
@@ -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: 'TONGUE_LOCALE',
8
+ name: LOCALE_COOKIE_NAME,
7
9
  maxAge: 60 * 60 * 24 * 365,
8
10
  sameSite: 'lax',
9
11
  path: '/',
@@ -61,3 +61,5 @@ export type { Format, SanctionedUnit, SimpleUnit, DisplayNameType, } from '../ru
61
61
  export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
62
62
  export { setLocalePreference, } from './locale-preference.js';
63
63
  export type { SetLocalePreferenceOptions, CookieStoreLike, } from './locale-preference.js';
64
+ export { requestLocale, LOCALE_COOKIE_NAME, LOCALE_HEADER_NAME, } from './request-locale.js';
65
+ export type { RequestLocaleOptions, RequestLocaleSource, } from './request-locale.js';
@@ -56,7 +56,7 @@ export async function getT(localeOrNs, namespace) {
56
56
  async function getTWithLocale(locale, ns) {
57
57
  assertLocale(locale);
58
58
  if (!namespaceLoader) {
59
- throw new TongueError('TONGUE_E_TYPES_MISSING', 'Message modules not loaded. Call ensureTongue() from `#tongue/runtime` (or wrap next.config with withTongue()).');
59
+ throw new TongueError('TONGUE_E_TYPES_MISSING', 'Message modules not loaded. Import ensureTongue from `#tongue/runtime` and call it once (layout/page), or wrap next.config with withTongue() and run `tongue build && next build` so `.tongue/` is fresh.');
60
60
  }
61
61
  const messages = await namespaceLoader(ns);
62
62
  return createT({
@@ -141,6 +141,7 @@ export { generateAlternates, createGenerateAlternates, } from './alternates.js';
141
141
  export { translateIssues, defaultFormatPath, } from '../runtime/translate-issues.js';
142
142
  export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
143
143
  export { setLocalePreference, } from './locale-preference.js';
144
+ export { requestLocale, LOCALE_COOKIE_NAME, LOCALE_HEADER_NAME, } from './request-locale.js';
144
145
  function assertLocale(locale) {
145
146
  if (runtimeLocales.length && !runtimeLocales.includes(locale)) {
146
147
  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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-tongue",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "description": "Compiler-first, type-safe i18n for the Next.js 16 App Router",
5
5
  "license": "MIT",
6
6
  "type": "module",