@mintlify/common 1.0.1229 → 1.0.1230

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.
@@ -270,6 +270,29 @@ describe(filterDivisions, () => {
270
270
  });
271
271
  });
272
272
  });
273
+ describe('filterDivisions with regional languages', () => {
274
+ it('matches the same page in a regional language whose prefix is lower case', () => {
275
+ const page = (href) => ({ title: href, href });
276
+ const result = filterDivisions({
277
+ currentPath: '/quickstart',
278
+ userGroups: new Set(),
279
+ currentVersion: undefined,
280
+ currentLanguage: 'en',
281
+ shouldUseDivisionMatch: false,
282
+ cache: { dropdownCache: new Map() },
283
+ decoratedNav: {
284
+ languages: [
285
+ { language: 'en', default: true, pages: [page('/index'), page('/quickstart')] },
286
+ { language: 'en-GB', pages: [page('/en-gb/index'), page('/en-gb/quickstart')] },
287
+ ],
288
+ },
289
+ });
290
+ expect(result.firstHrefInLanguage.get('en-GB')).toEqual({
291
+ href: '/en-gb/quickstart',
292
+ matchLevel: DivisionMatchLevel.PATH,
293
+ });
294
+ });
295
+ });
273
296
  describe('filterDivisions with partial auth', () => {
274
297
  const defaultArgs = {
275
298
  decoratedNav: { groups: [] },
@@ -9,6 +9,23 @@ describe('pathsDifferByOneSegment', () => {
9
9
  expect(pathsDifferByOneSegment('/ja/getting-started', '/es/getting-started', 'language')).toBe(true);
10
10
  expect(pathsDifferByOneSegment('/fr/docs/intro', '/de/docs/intro', 'language')).toBe(true);
11
11
  });
12
+ it('treats configured regional language prefixes as locales, ignoring case', () => {
13
+ const languages = ['en', 'en-GB', 'es', 'es-MX'];
14
+ expect(pathsDifferByOneSegment('/en-gb/quickstart', '/quickstart', 'language', languages)).toBe(true);
15
+ expect(pathsDifferByOneSegment('/es-MX/quickstart', '/es/quickstart', 'language', languages)).toBe(true);
16
+ });
17
+ it('treats a configured regional language after a version segment as a locale', () => {
18
+ expect(pathsDifferByOneSegment('/v2/en-gb/quickstart', '/v2/quickstart', 'language', [
19
+ 'en',
20
+ 'en-GB',
21
+ ])).toBe(true);
22
+ expect(pathsDifferByOneSegment('/v2/hi-fi/quickstart', '/v2/quickstart', 'language')).toBe(false);
23
+ });
24
+ it('does not treat unconfigured code-like segments as locales', () => {
25
+ expect(pathsDifferByOneSegment('/en-gb/quickstart', '/quickstart', 'language')).toBe(false);
26
+ expect(pathsDifferByOneSegment('/hi-fi/setup', '/de/hi-fi/setup', 'language', ['en', 'de'])).toBe(true);
27
+ expect(pathsDifferByOneSegment('/hi-fi/setup', '/de/setup', 'language', ['en', 'de'])).toBe(false);
28
+ });
12
29
  it('returns false for paths with different content', () => {
13
30
  expect(pathsDifferByOneSegment('/ja/getting-started', '/ja/quickstart', 'language')).toBe(false);
14
31
  expect(pathsDifferByOneSegment('/getting-started', '/quickstart', 'language')).toBe(false);
@@ -1,5 +1,5 @@
1
1
  import { LocaleType } from '@mintlify/models';
2
- import type { DecoratedNavigationConfig, DocsConfig } from '@mintlify/validation';
2
+ import { type DecoratedNavigationConfig, type DocsConfig } from '@mintlify/validation';
3
3
  import { DivisionMatch } from './updateFirstHrefInDivision.js';
4
4
  export declare function scopeDecoratedNavToPath(nav: DecoratedNavigationConfig, currentPath: string, userGroups?: Set<string>): DecoratedNavigationConfig;
5
5
  export declare function scopeDocsConfigNavToPath(docsConfig: DocsConfig, currentPath: string): DocsConfig;
@@ -9,7 +9,8 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { locales } from '@mintlify/models';
12
+ import { isLocale } from '@mintlify/models';
13
+ import { divisions, } from '@mintlify/validation';
13
14
  import { generatePathToLanguageDict, generatePathToVersionDict } from '../divisions/index.js';
14
15
  import { optionallyRemoveLeadingSlash } from '../fs/optionallySlash.js';
15
16
  import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
@@ -49,6 +50,31 @@ const STUB_STRIPPED_KEYS = new Set([
49
50
  function isNavNode(value) {
50
51
  return typeof value === 'object' && value !== null && !Array.isArray(value);
51
52
  }
53
+ const LANGUAGE_CONTAINER_KEYS = [...divisions, 'global'];
54
+ function addConfiguredLanguages(node, languages) {
55
+ if (Array.isArray(node)) {
56
+ for (const entry of node)
57
+ addConfiguredLanguages(entry, languages);
58
+ return;
59
+ }
60
+ if (!isNavNode(node))
61
+ return;
62
+ if (typeof node.language === 'string' && isLocale(node.language))
63
+ languages.add(node.language);
64
+ for (const key of LANGUAGE_CONTAINER_KEYS)
65
+ addConfiguredLanguages(node[key], languages);
66
+ }
67
+ const configuredLanguagesCache = new WeakMap();
68
+ function getConfiguredLanguages(nav) {
69
+ const cached = configuredLanguagesCache.get(nav);
70
+ if (cached)
71
+ return cached;
72
+ const languages = new Set();
73
+ addConfiguredLanguages(nav, languages);
74
+ const result = [...languages];
75
+ configuredLanguagesCache.set(nav, result);
76
+ return result;
77
+ }
52
78
  function isScopedDivisionKey(key) {
53
79
  return key === 'versions' || key === 'languages';
54
80
  }
@@ -143,7 +169,7 @@ function pickStubTarget(division, ctx, type) {
143
169
  const exact = pool.find((candidate) => isEqualIgnoringLeadingSlash(candidate.href, ctx.currentPath));
144
170
  if (exact)
145
171
  return exact;
146
- const pathMatch = pool.find((candidate) => pathsDifferByOneSegment(candidate.href, ctx.currentPath, type));
172
+ const pathMatch = pool.find((candidate) => pathsDifferByOneSegment(candidate.href, ctx.currentPath, type, ctx.languages));
147
173
  return pathMatch !== null && pathMatch !== void 0 ? pathMatch : pool[0];
148
174
  }
149
175
  // carrying the target's hidden/groups keeps a division with no anonymously
@@ -517,13 +543,18 @@ function scopeNode(node, ctx) {
517
543
  return scoped;
518
544
  }
519
545
  export function scopeDecoratedNavToPath(nav, currentPath, userGroups) {
546
+ const ctx = { currentPath, userGroups, languages: getConfiguredLanguages(nav) };
520
547
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
521
- return scopeNode(nav, { currentPath, userGroups });
548
+ return scopeNode(nav, ctx);
522
549
  }
523
550
  export function scopeDocsConfigNavToPath(docsConfig, currentPath) {
551
+ const ctx = {
552
+ currentPath,
553
+ languages: getConfiguredLanguages(docsConfig.navigation),
554
+ };
524
555
  return Object.assign(Object.assign({}, docsConfig), {
525
556
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- structural nav traversal over the zod union types
526
- navigation: scopeNode(docsConfig.navigation, { currentPath }) });
557
+ navigation: scopeNode(docsConfig.navigation, ctx) });
527
558
  }
528
559
  const pathToVersionDictCache = new WeakMap();
529
560
  const pathToLanguageDictCache = new WeakMap();
@@ -571,7 +602,7 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
571
602
  const strippedPath = optionallyRemoveLeadingSlash(currentPath);
572
603
  const dictVersion = cachedPathToVersionDict(decoratedNav).get(strippedPath);
573
604
  const dictLanguage = cachedPathToLanguageDict(decoratedNav).get(strippedPath);
574
- const localeFromDict = locales.find((locale) => locale === dictLanguage);
605
+ const localeFromDict = dictLanguage && isLocale(dictLanguage) ? dictLanguage : undefined;
575
606
  currentVersion =
576
607
  dictVersion ||
577
608
  getVersionOrLanguageFromPath('version', decoratedNav, currentPath, defaultVersion, (_d = (_c = initial.versions[0]) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '') ||
@@ -596,6 +627,7 @@ export function getScopedNavForPath({ decoratedNav, currentPath, userGroups = ne
596
627
  currentPath,
597
628
  currentVersion,
598
629
  currentLanguage,
630
+ languages: getConfiguredLanguages(decoratedNav),
599
631
  versionTargets: filtered.firstHrefInVersion,
600
632
  languageTargets: filtered.firstHrefInLanguage,
601
633
  userGroups,
@@ -43,6 +43,43 @@ describe('scopeDecoratedNavToPath', () => {
43
43
  expect(es.pages).toEqual([{ title: 'es', href: '/es/get-started/intro' }]);
44
44
  expect(fr.pages).toEqual([{ title: 'fr', href: '/fr/other/page' }]);
45
45
  });
46
+ it('keeps a regional language division active on its own pages', () => {
47
+ const regionalNav = {
48
+ languages: [
49
+ { language: 'en', default: true, pages: [page('/intro'), page('/install')] },
50
+ { language: 'en-GB', pages: [page('/en-gb/intro'), page('/en-gb/install')] },
51
+ ],
52
+ };
53
+ const [en, enGB] = loose(scopeDecoratedNavToPath(regionalNav, '/en-gb/intro')).languages;
54
+ if (!en || !enGB)
55
+ throw new Error('missing languages');
56
+ expect(enGB.pages).toEqual([page('/en-gb/intro'), page('/en-gb/install')]);
57
+ expect(en.pages).toEqual([{ title: 'en', href: '/intro' }]);
58
+ });
59
+ it('stubs a regional language division with the same page under its prefix', () => {
60
+ const regionalNav = {
61
+ languages: [
62
+ { language: 'en', default: true, pages: [page('/intro'), page('/install')] },
63
+ { language: 'en-GB', pages: [page('/en-gb/intro'), page('/en-gb/install')] },
64
+ ],
65
+ };
66
+ const [, enGB] = loose(scopeDecoratedNavToPath(regionalNav, '/install')).languages;
67
+ expect(enGB === null || enGB === void 0 ? void 0 : enGB.pages).toEqual([{ title: 'en-GB', href: '/en-gb/install' }]);
68
+ });
69
+ it('ignores a page field named language when finding configured languages', () => {
70
+ const nav = {
71
+ languages: [
72
+ {
73
+ language: 'en',
74
+ default: true,
75
+ pages: [{ title: 'Setup', href: '/hi-fi/setup', language: 'hi-fi' }],
76
+ },
77
+ { language: 'de', pages: [page('/de/setup'), page('/de/hi-fi/setup')] },
78
+ ],
79
+ };
80
+ const [, de] = loose(scopeDecoratedNavToPath(nav, '/hi-fi/setup')).languages;
81
+ expect(de === null || de === void 0 ? void 0 : de.pages).toEqual([{ title: 'de', href: '/de/hi-fi/setup' }]);
82
+ });
46
83
  it('falls back to the default division when the path is not found', () => {
47
84
  const scoped = scopeDecoratedNavToPath(decoratedNav, '/does/not/exist');
48
85
  const [en, es] = loose(scoped).languages;
@@ -28,4 +28,4 @@ export declare function updateFirstHrefInDivision({ type, currentPath, entry, pa
28
28
  activeGroupNames?: Set<string>;
29
29
  activeDivisionOwner?: object;
30
30
  }): void;
31
- export declare function pathsDifferByOneSegment(a: string, b: string, type: 'version' | 'language'): boolean;
31
+ export declare function pathsDifferByOneSegment(a: string, b: string, type: 'version' | 'language', languages?: readonly string[]): boolean;
@@ -1,4 +1,4 @@
1
- import { locales } from '@mintlify/models';
1
+ import { isKnownLocale } from '@mintlify/models';
2
2
  import { divisions, } from '@mintlify/validation';
3
3
  import { readObjectArrayProperty } from '../divisions/iterateObjectArrayProperty.js';
4
4
  import { isEqualIgnoringLeadingSlash } from '../paths/isEqualIgnoringLeadingSlash.js';
@@ -209,29 +209,30 @@ function findActiveGroupNames(entry, currentPath, activeGroupNames = new Set())
209
209
  }
210
210
  return activeGroupNames;
211
211
  }
212
- function parseLanguagePath(segments) {
212
+ const isLanguageSegment = (segment, languages) => {
213
+ if (segment === undefined)
214
+ return false;
215
+ if (isKnownLocale(segment))
216
+ return true;
217
+ const lowerCaseSegment = segment.toLowerCase();
218
+ return languages.some((language) => language.toLowerCase() === lowerCaseSegment);
219
+ };
220
+ function parseLanguagePath(segments, languages) {
213
221
  let index = 0;
214
222
  let locale;
215
223
  let hasDocsBase = false;
216
224
  const firstSegment = segments[0];
217
225
  const firstIsDocsBase = !!firstSegment && ACCEPTED_BASE_PATHS.includes(firstSegment);
218
- const firstIsLocale = !!firstSegment &&
219
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
220
- locales.includes(firstSegment);
221
226
  if (firstIsDocsBase) {
222
227
  hasDocsBase = true;
223
228
  index = 1;
224
229
  const maybeLocale = segments[index];
225
- if (maybeLocale &&
226
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
227
- locales.includes(maybeLocale)) {
228
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
230
+ if (isLanguageSegment(maybeLocale, languages)) {
229
231
  locale = maybeLocale;
230
232
  index++;
231
233
  }
232
234
  }
233
- else if (firstIsLocale) {
234
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TODO: Please fix this violation when you can!
235
+ else if (isLanguageSegment(firstSegment, languages)) {
235
236
  locale = firstSegment;
236
237
  index = 1;
237
238
  const maybeDocsBase = segments[index];
@@ -283,8 +284,9 @@ function maybeUpdateFirstHrefInDivision(type, href, finalVersion, inActiveGroup,
283
284
  // we make the BOLD assumption that href is a different version of the same page
284
285
  if (currentVersion !== undefined && finalVersion !== undefined) {
285
286
  const existingRef = firstHrefInDivision.get(finalVersion);
287
+ const languages = type === 'language' ? versionStrings : undefined;
286
288
  if ((existingRef === undefined || DivisionMatchLevel.PATH < existingRef.matchLevel) &&
287
- pathsDifferByOneSegment(href, currentPath, type)) {
289
+ pathsDifferByOneSegment(href, currentPath, type, languages)) {
288
290
  firstHrefInDivision.set(finalVersion, { href, matchLevel: DivisionMatchLevel.PATH });
289
291
  }
290
292
  }
@@ -316,18 +318,19 @@ function maybeUpdateFirstHrefInDivision(type, href, finalVersion, inActiveGroup,
316
318
  }
317
319
  }
318
320
  }
319
- export function pathsDifferByOneSegment(a, b, type) {
321
+ export function pathsDifferByOneSegment(a, b, type, languages = []) {
322
+ var _a, _b;
320
323
  const aSegments = a.split('/').filter(Boolean);
321
324
  const bSegments = b.split('/').filter(Boolean);
322
325
  // For language switching, we need to be more strict - paths should be identical except for the language segment
323
326
  // once normalized, content segments should be identical and only locale prefix may differ.
324
327
  if (type === 'language') {
325
- const aPath = parseLanguagePath(aSegments);
326
- const bPath = parseLanguagePath(bSegments);
328
+ const aPath = parseLanguagePath(aSegments, languages);
329
+ const bPath = parseLanguagePath(bSegments, languages);
327
330
  if (aPath.hasDocsBase !== bPath.hasDocsBase)
328
331
  return false;
329
332
  if (!aPath.locale && !bPath.locale) {
330
- return differByOneLocaleSegment(aPath.contentSegments, bPath.contentSegments);
333
+ return differByOneLocaleSegment(aPath.contentSegments, bPath.contentSegments, languages);
331
334
  }
332
335
  // For language switching, the content parts (including version segments) must be identical.
333
336
  if (aPath.contentSegments.length !== bPath.contentSegments.length)
@@ -340,7 +343,7 @@ export function pathsDifferByOneSegment(a, b, type) {
340
343
  // The paths are language variants if locale presence/value differs.
341
344
  if (!aPath.locale && !bPath.locale)
342
345
  return false;
343
- return aPath.locale !== bPath.locale;
346
+ return ((_a = aPath.locale) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== ((_b = bPath.locale) === null || _b === void 0 ? void 0 : _b.toLowerCase());
344
347
  }
345
348
  const aStartsWithDocs = aSegments[0] && ACCEPTED_BASE_PATHS.includes(aSegments[0]);
346
349
  const bStartsWithDocs = bSegments[0] && ACCEPTED_BASE_PATHS.includes(bSegments[0]);
@@ -377,22 +380,22 @@ export function pathsDifferByOneSegment(a, b, type) {
377
380
  }
378
381
  return true;
379
382
  }
380
- function isLocale(segment) {
381
- return locales.some((locale) => locale === segment);
382
- }
383
- function differByOneLocaleSegment(a, b) {
383
+ function differByOneLocaleSegment(a, b, languages) {
384
384
  if (a.length === b.length) {
385
385
  const mismatches = a.flatMap((segment, i) => (segment === b[i] ? [] : [i]));
386
386
  if (mismatches.length !== 1)
387
387
  return false;
388
388
  const [index] = mismatches;
389
- return index !== undefined && isLocale(a[index]) && isLocale(b[index]);
389
+ return (index !== undefined &&
390
+ isLanguageSegment(a[index], languages) &&
391
+ isLanguageSegment(b[index], languages));
390
392
  }
391
393
  const [longer, shorter] = a.length > b.length ? [a, b] : [b, a];
392
394
  if (longer.length !== shorter.length + 1)
393
395
  return false;
394
396
  const index = longer.findIndex((segment, i) => segment !== shorter[i]);
395
- return (isLocale(longer[index]) && longer.slice(index + 1).every((s, i) => s === shorter[index + i]));
397
+ return (isLanguageSegment(longer[index], languages) &&
398
+ longer.slice(index + 1).every((s, i) => s === shorter[index + i]));
396
399
  }
397
400
  function checkActiveGroup(group, currentPath) {
398
401
  if (isPage(group)) {