@tldraw/tlschema 4.1.0-next.542f014c3fac → 4.1.0-next.cb6f90590225

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.
Files changed (212) hide show
  1. package/dist-cjs/TLStore.js +3 -10
  2. package/dist-cjs/TLStore.js.map +2 -2
  3. package/dist-cjs/assets/TLBaseAsset.js.map +2 -2
  4. package/dist-cjs/assets/TLBookmarkAsset.js.map +2 -2
  5. package/dist-cjs/assets/TLImageAsset.js.map +2 -2
  6. package/dist-cjs/assets/TLVideoAsset.js.map +2 -2
  7. package/dist-cjs/bindings/TLArrowBinding.js.map +2 -2
  8. package/dist-cjs/bindings/TLBaseBinding.js.map +2 -2
  9. package/dist-cjs/createPresenceStateDerivation.js.map +2 -2
  10. package/dist-cjs/createTLSchema.js.map +2 -2
  11. package/dist-cjs/index.d.ts +4416 -223
  12. package/dist-cjs/index.js +1 -1
  13. package/dist-cjs/index.js.map +2 -2
  14. package/dist-cjs/misc/TLColor.js.map +2 -2
  15. package/dist-cjs/misc/TLCursor.js.map +2 -2
  16. package/dist-cjs/misc/TLHandle.js.map +2 -2
  17. package/dist-cjs/misc/TLOpacity.js.map +2 -2
  18. package/dist-cjs/misc/TLRichText.js.map +2 -2
  19. package/dist-cjs/misc/TLScribble.js.map +2 -2
  20. package/dist-cjs/misc/geometry-types.js.map +2 -2
  21. package/dist-cjs/misc/id-validator.js.map +2 -2
  22. package/dist-cjs/records/TLAsset.js.map +2 -2
  23. package/dist-cjs/records/TLBinding.js.map +2 -2
  24. package/dist-cjs/records/TLCamera.js.map +2 -2
  25. package/dist-cjs/records/TLDocument.js.map +2 -2
  26. package/dist-cjs/records/TLInstance.js.map +2 -2
  27. package/dist-cjs/records/TLPage.js.map +2 -2
  28. package/dist-cjs/records/TLPageState.js.map +2 -2
  29. package/dist-cjs/records/TLPointer.js.map +2 -2
  30. package/dist-cjs/records/TLPresence.js.map +2 -2
  31. package/dist-cjs/records/TLRecord.js.map +1 -1
  32. package/dist-cjs/records/TLShape.js.map +2 -2
  33. package/dist-cjs/recordsWithProps.js.map +2 -2
  34. package/dist-cjs/shapes/ShapeWithCrop.js.map +1 -1
  35. package/dist-cjs/shapes/TLArrowShape.js.map +2 -2
  36. package/dist-cjs/shapes/TLBaseShape.js.map +2 -2
  37. package/dist-cjs/shapes/TLBookmarkShape.js.map +2 -2
  38. package/dist-cjs/shapes/TLDrawShape.js.map +2 -2
  39. package/dist-cjs/shapes/TLEmbedShape.js.map +2 -2
  40. package/dist-cjs/shapes/TLFrameShape.js.map +2 -2
  41. package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
  42. package/dist-cjs/shapes/TLGroupShape.js.map +2 -2
  43. package/dist-cjs/shapes/TLHighlightShape.js.map +2 -2
  44. package/dist-cjs/shapes/TLImageShape.js.map +2 -2
  45. package/dist-cjs/shapes/TLLineShape.js.map +2 -2
  46. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  47. package/dist-cjs/shapes/TLTextShape.js.map +2 -2
  48. package/dist-cjs/shapes/TLVideoShape.js.map +2 -2
  49. package/dist-cjs/store-migrations.js.map +2 -2
  50. package/dist-cjs/styles/TLColorStyle.js.map +2 -2
  51. package/dist-cjs/styles/TLDashStyle.js.map +2 -2
  52. package/dist-cjs/styles/TLFillStyle.js.map +2 -2
  53. package/dist-cjs/styles/TLFontStyle.js.map +2 -2
  54. package/dist-cjs/styles/TLHorizontalAlignStyle.js.map +2 -2
  55. package/dist-cjs/styles/TLSizeStyle.js.map +2 -2
  56. package/dist-cjs/styles/TLTextAlignStyle.js.map +2 -2
  57. package/dist-cjs/styles/TLVerticalAlignStyle.js.map +2 -2
  58. package/dist-cjs/translations/translations.js +1 -1
  59. package/dist-cjs/translations/translations.js.map +2 -2
  60. package/dist-cjs/util-types.js.map +1 -1
  61. package/dist-esm/TLStore.mjs +3 -10
  62. package/dist-esm/TLStore.mjs.map +2 -2
  63. package/dist-esm/assets/TLBaseAsset.mjs.map +2 -2
  64. package/dist-esm/assets/TLBookmarkAsset.mjs.map +2 -2
  65. package/dist-esm/assets/TLImageAsset.mjs.map +2 -2
  66. package/dist-esm/assets/TLVideoAsset.mjs.map +2 -2
  67. package/dist-esm/bindings/TLArrowBinding.mjs.map +2 -2
  68. package/dist-esm/bindings/TLBaseBinding.mjs.map +2 -2
  69. package/dist-esm/createPresenceStateDerivation.mjs.map +2 -2
  70. package/dist-esm/createTLSchema.mjs.map +2 -2
  71. package/dist-esm/index.d.mts +4416 -223
  72. package/dist-esm/index.mjs +1 -1
  73. package/dist-esm/index.mjs.map +2 -2
  74. package/dist-esm/misc/TLColor.mjs.map +2 -2
  75. package/dist-esm/misc/TLCursor.mjs.map +2 -2
  76. package/dist-esm/misc/TLHandle.mjs.map +2 -2
  77. package/dist-esm/misc/TLOpacity.mjs.map +2 -2
  78. package/dist-esm/misc/TLRichText.mjs.map +2 -2
  79. package/dist-esm/misc/TLScribble.mjs.map +2 -2
  80. package/dist-esm/misc/geometry-types.mjs.map +2 -2
  81. package/dist-esm/misc/id-validator.mjs.map +2 -2
  82. package/dist-esm/records/TLAsset.mjs.map +2 -2
  83. package/dist-esm/records/TLBinding.mjs.map +2 -2
  84. package/dist-esm/records/TLCamera.mjs.map +2 -2
  85. package/dist-esm/records/TLDocument.mjs.map +2 -2
  86. package/dist-esm/records/TLInstance.mjs.map +2 -2
  87. package/dist-esm/records/TLPage.mjs.map +2 -2
  88. package/dist-esm/records/TLPageState.mjs.map +2 -2
  89. package/dist-esm/records/TLPointer.mjs.map +2 -2
  90. package/dist-esm/records/TLPresence.mjs.map +2 -2
  91. package/dist-esm/records/TLShape.mjs.map +2 -2
  92. package/dist-esm/recordsWithProps.mjs.map +2 -2
  93. package/dist-esm/shapes/TLArrowShape.mjs.map +2 -2
  94. package/dist-esm/shapes/TLBaseShape.mjs.map +2 -2
  95. package/dist-esm/shapes/TLBookmarkShape.mjs.map +2 -2
  96. package/dist-esm/shapes/TLDrawShape.mjs.map +2 -2
  97. package/dist-esm/shapes/TLEmbedShape.mjs.map +2 -2
  98. package/dist-esm/shapes/TLFrameShape.mjs.map +2 -2
  99. package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
  100. package/dist-esm/shapes/TLGroupShape.mjs.map +2 -2
  101. package/dist-esm/shapes/TLHighlightShape.mjs.map +2 -2
  102. package/dist-esm/shapes/TLImageShape.mjs.map +2 -2
  103. package/dist-esm/shapes/TLLineShape.mjs.map +2 -2
  104. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  105. package/dist-esm/shapes/TLTextShape.mjs.map +2 -2
  106. package/dist-esm/shapes/TLVideoShape.mjs.map +2 -2
  107. package/dist-esm/store-migrations.mjs.map +2 -2
  108. package/dist-esm/styles/TLColorStyle.mjs.map +2 -2
  109. package/dist-esm/styles/TLDashStyle.mjs.map +2 -2
  110. package/dist-esm/styles/TLFillStyle.mjs.map +2 -2
  111. package/dist-esm/styles/TLFontStyle.mjs.map +2 -2
  112. package/dist-esm/styles/TLHorizontalAlignStyle.mjs.map +2 -2
  113. package/dist-esm/styles/TLSizeStyle.mjs.map +2 -2
  114. package/dist-esm/styles/TLTextAlignStyle.mjs.map +2 -2
  115. package/dist-esm/styles/TLVerticalAlignStyle.mjs.map +2 -2
  116. package/dist-esm/translations/translations.mjs +1 -1
  117. package/dist-esm/translations/translations.mjs.map +2 -2
  118. package/package.json +5 -5
  119. package/src/TLStore.test.ts +644 -0
  120. package/src/TLStore.ts +205 -20
  121. package/src/assets/TLBaseAsset.ts +90 -7
  122. package/src/assets/TLBookmarkAsset.test.ts +96 -0
  123. package/src/assets/TLBookmarkAsset.ts +52 -2
  124. package/src/assets/TLImageAsset.test.ts +213 -0
  125. package/src/assets/TLImageAsset.ts +60 -2
  126. package/src/assets/TLVideoAsset.test.ts +105 -0
  127. package/src/assets/TLVideoAsset.ts +93 -4
  128. package/src/bindings/TLArrowBinding.test.ts +55 -0
  129. package/src/bindings/TLArrowBinding.ts +132 -10
  130. package/src/bindings/TLBaseBinding.ts +140 -3
  131. package/src/createPresenceStateDerivation.test.ts +158 -0
  132. package/src/createPresenceStateDerivation.ts +71 -2
  133. package/src/createTLSchema.test.ts +181 -0
  134. package/src/createTLSchema.ts +164 -7
  135. package/src/index.ts +32 -0
  136. package/src/misc/TLColor.ts +50 -6
  137. package/src/misc/TLCursor.ts +110 -8
  138. package/src/misc/TLHandle.ts +86 -6
  139. package/src/misc/TLOpacity.ts +51 -2
  140. package/src/misc/TLRichText.ts +56 -3
  141. package/src/misc/TLScribble.ts +105 -5
  142. package/src/misc/geometry-types.ts +30 -2
  143. package/src/misc/id-validator.test.ts +50 -0
  144. package/src/misc/id-validator.ts +20 -1
  145. package/src/records/TLAsset.test.ts +234 -0
  146. package/src/records/TLAsset.ts +165 -8
  147. package/src/records/TLBinding.test.ts +22 -0
  148. package/src/records/TLBinding.ts +277 -11
  149. package/src/records/TLCamera.test.ts +19 -0
  150. package/src/records/TLCamera.ts +118 -7
  151. package/src/records/TLDocument.test.ts +35 -0
  152. package/src/records/TLDocument.ts +148 -8
  153. package/src/records/TLInstance.test.ts +201 -0
  154. package/src/records/TLInstance.ts +117 -9
  155. package/src/records/TLPage.test.ts +110 -0
  156. package/src/records/TLPage.ts +106 -8
  157. package/src/records/TLPageState.test.ts +228 -0
  158. package/src/records/TLPageState.ts +88 -7
  159. package/src/records/TLPointer.test.ts +63 -0
  160. package/src/records/TLPointer.ts +105 -7
  161. package/src/records/TLPresence.test.ts +190 -0
  162. package/src/records/TLPresence.ts +99 -5
  163. package/src/records/TLRecord.test.ts +70 -0
  164. package/src/records/TLRecord.ts +43 -1
  165. package/src/records/TLShape.test.ts +232 -0
  166. package/src/records/TLShape.ts +289 -12
  167. package/src/recordsWithProps.test.ts +188 -0
  168. package/src/recordsWithProps.ts +131 -2
  169. package/src/shapes/ShapeWithCrop.test.ts +18 -0
  170. package/src/shapes/ShapeWithCrop.ts +64 -2
  171. package/src/shapes/TLArrowShape.test.ts +505 -0
  172. package/src/shapes/TLArrowShape.ts +188 -10
  173. package/src/shapes/TLBaseShape.test.ts +142 -0
  174. package/src/shapes/TLBaseShape.ts +103 -4
  175. package/src/shapes/TLBookmarkShape.test.ts +122 -0
  176. package/src/shapes/TLBookmarkShape.ts +58 -4
  177. package/src/shapes/TLDrawShape.test.ts +177 -0
  178. package/src/shapes/TLDrawShape.ts +97 -6
  179. package/src/shapes/TLEmbedShape.test.ts +286 -0
  180. package/src/shapes/TLEmbedShape.ts +57 -4
  181. package/src/shapes/TLFrameShape.test.ts +71 -0
  182. package/src/shapes/TLFrameShape.ts +59 -4
  183. package/src/shapes/TLGeoShape.test.ts +247 -0
  184. package/src/shapes/TLGeoShape.ts +103 -7
  185. package/src/shapes/TLGroupShape.test.ts +59 -0
  186. package/src/shapes/TLGroupShape.ts +52 -4
  187. package/src/shapes/TLHighlightShape.test.ts +325 -0
  188. package/src/shapes/TLHighlightShape.ts +79 -4
  189. package/src/shapes/TLImageShape.test.ts +534 -0
  190. package/src/shapes/TLImageShape.ts +105 -5
  191. package/src/shapes/TLLineShape.test.ts +269 -0
  192. package/src/shapes/TLLineShape.ts +128 -8
  193. package/src/shapes/TLNoteShape.test.ts +1568 -0
  194. package/src/shapes/TLNoteShape.ts +97 -4
  195. package/src/shapes/TLTextShape.test.ts +407 -0
  196. package/src/shapes/TLTextShape.ts +94 -4
  197. package/src/shapes/TLVideoShape.test.ts +112 -0
  198. package/src/shapes/TLVideoShape.ts +99 -4
  199. package/src/store-migrations.test.ts +88 -0
  200. package/src/store-migrations.ts +47 -1
  201. package/src/styles/TLColorStyle.test.ts +439 -0
  202. package/src/styles/TLColorStyle.ts +228 -10
  203. package/src/styles/TLDashStyle.ts +54 -2
  204. package/src/styles/TLFillStyle.ts +54 -2
  205. package/src/styles/TLFontStyle.ts +72 -3
  206. package/src/styles/TLHorizontalAlignStyle.ts +55 -2
  207. package/src/styles/TLSizeStyle.ts +54 -2
  208. package/src/styles/TLTextAlignStyle.ts +52 -2
  209. package/src/styles/TLVerticalAlignStyle.ts +52 -2
  210. package/src/translations/translations.test.ts +378 -35
  211. package/src/translations/translations.ts +157 -10
  212. package/src/util-types.ts +51 -1
@@ -1,11 +1,61 @@
1
1
  import { T } from '@tldraw/validate'
2
2
  import { StyleProp } from './StyleProp'
3
3
 
4
- /** @public */
4
+ /**
5
+ * Default text alignment style property used by tldraw text shapes.
6
+ * Controls how text content is aligned within text-based shapes like text boxes and notes.
7
+ *
8
+ * Available values:
9
+ * - `start` - Align text to the start (left in LTR, right in RTL)
10
+ * - `middle` - Center text horizontally
11
+ * - `end` - Align text to the end (right in LTR, left in RTL)
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { DefaultTextAlignStyle } from '@tldraw/tlschema'
16
+ *
17
+ * // Use in text shape props definition
18
+ * interface MyTextShapeProps {
19
+ * textAlign: typeof DefaultTextAlignStyle
20
+ * // other props...
21
+ * }
22
+ *
23
+ * // Create a text shape with center alignment
24
+ * const textShape = {
25
+ * // ... other properties
26
+ * props: {
27
+ * textAlign: 'middle' as const,
28
+ * // ... other props
29
+ * }
30
+ * }
31
+ * ```
32
+ *
33
+ * @public
34
+ */
5
35
  export const DefaultTextAlignStyle = StyleProp.defineEnum('tldraw:textAlign', {
6
36
  defaultValue: 'start',
7
37
  values: ['start', 'middle', 'end'],
8
38
  })
9
39
 
10
- /** @public */
40
+ /**
41
+ * Type representing a default text alignment style value.
42
+ * This is a union type of all available text alignment options.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { TLDefaultTextAlignStyle } from '@tldraw/tlschema'
47
+ *
48
+ * // Valid text alignment values
49
+ * const leftAlign: TLDefaultTextAlignStyle = 'start'
50
+ * const centerAlign: TLDefaultTextAlignStyle = 'middle'
51
+ * const rightAlign: TLDefaultTextAlignStyle = 'end'
52
+ *
53
+ * // Use in a function parameter
54
+ * function setTextAlignment(align: TLDefaultTextAlignStyle) {
55
+ * // Apply text alignment to text shape
56
+ * }
57
+ * ```
58
+ *
59
+ * @public
60
+ */
11
61
  export type TLDefaultTextAlignStyle = T.TypeOf<typeof DefaultTextAlignStyle>
@@ -1,11 +1,61 @@
1
1
  import { T } from '@tldraw/validate'
2
2
  import { StyleProp } from './StyleProp'
3
3
 
4
- /** @public */
4
+ /**
5
+ * Default vertical alignment style property used by tldraw shapes for text positioning.
6
+ * Controls how text content is vertically aligned within shape boundaries.
7
+ *
8
+ * Available values:
9
+ * - `start` - Align text to the top
10
+ * - `middle` - Center text vertically (default)
11
+ * - `end` - Align text to the bottom
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { DefaultVerticalAlignStyle } from '@tldraw/tlschema'
16
+ *
17
+ * // Use in shape props definition
18
+ * interface MyShapeProps {
19
+ * verticalAlign: typeof DefaultVerticalAlignStyle
20
+ * // other props...
21
+ * }
22
+ *
23
+ * // Create a shape with top-aligned text
24
+ * const shape = {
25
+ * // ... other properties
26
+ * props: {
27
+ * verticalAlign: 'start' as const,
28
+ * // ... other props
29
+ * }
30
+ * }
31
+ * ```
32
+ *
33
+ * @public
34
+ */
5
35
  export const DefaultVerticalAlignStyle = StyleProp.defineEnum('tldraw:verticalAlign', {
6
36
  defaultValue: 'middle',
7
37
  values: ['start', 'middle', 'end'],
8
38
  })
9
39
 
10
- /** @public */
40
+ /**
41
+ * Type representing a default vertical alignment style value.
42
+ * This is a union type of all available vertical alignment options.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { TLDefaultVerticalAlignStyle } from '@tldraw/tlschema'
47
+ *
48
+ * // Valid vertical alignment values
49
+ * const topAlign: TLDefaultVerticalAlignStyle = 'start'
50
+ * const centerAlign: TLDefaultVerticalAlignStyle = 'middle'
51
+ * const bottomAlign: TLDefaultVerticalAlignStyle = 'end'
52
+ *
53
+ * // Use in a function parameter
54
+ * function setVerticalAlignment(align: TLDefaultVerticalAlignStyle) {
55
+ * // Apply vertical alignment to text
56
+ * }
57
+ * ```
58
+ *
59
+ * @public
60
+ */
11
61
  export type TLDefaultVerticalAlignStyle = T.TypeOf<typeof DefaultVerticalAlignStyle>
@@ -1,4 +1,10 @@
1
- import { _getDefaultTranslationLocale } from './translations'
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ LANGUAGES,
4
+ TLLanguage,
5
+ _getDefaultTranslationLocale,
6
+ getDefaultTranslationLocale,
7
+ } from './translations'
2
8
 
3
9
  interface DefaultLanguageTest {
4
10
  name: string
@@ -6,38 +12,375 @@ interface DefaultLanguageTest {
6
12
  output: string
7
13
  }
8
14
 
9
- describe('Choosing a sensible default translation locale', () => {
10
- const tests: DefaultLanguageTest[] = [
11
- {
12
- name: 'finds a matching language locale',
13
- input: ['fr'],
14
- output: 'fr',
15
- },
16
- {
17
- name: 'finds a matching region locale',
18
- input: ['pt-PT'],
19
- output: 'pt-pt',
20
- },
21
- {
22
- name: 'picks a region locale if no language locale available',
23
- input: ['pt'],
24
- output: 'pt-br',
25
- },
26
- {
27
- name: 'picks a language locale if no region locale available',
28
- input: ['fr-CA'],
29
- output: 'fr',
30
- },
31
- {
32
- name: 'picks the first language that loosely matches',
33
- input: ['fr-CA', 'pt-PT'],
34
- output: 'fr',
35
- },
36
- ]
37
-
38
- for (const test of tests) {
39
- it(test.name, () => {
40
- expect(_getDefaultTranslationLocale(test.input)).toEqual(test.output)
41
- })
42
- }
15
+ const originalWindow = global.window
16
+
17
+ describe('translations.ts', () => {
18
+ describe('exports', () => {
19
+ it('should export LANGUAGES constant', () => {
20
+ expect(LANGUAGES).toBeDefined()
21
+ expect(Array.isArray(LANGUAGES)).toBe(true)
22
+ expect(LANGUAGES.length).toBeGreaterThan(0)
23
+ })
24
+
25
+ it('should export TLLanguage type', () => {
26
+ // Test that TLLanguage type works correctly
27
+ const testLanguage: TLLanguage = LANGUAGES[0]
28
+ expect(testLanguage).toHaveProperty('locale')
29
+ expect(testLanguage).toHaveProperty('label')
30
+ expect(typeof testLanguage.locale).toBe('string')
31
+ expect(typeof testLanguage.label).toBe('string')
32
+ })
33
+
34
+ it('should export getDefaultTranslationLocale function', () => {
35
+ expect(typeof getDefaultTranslationLocale).toBe('function')
36
+ })
37
+
38
+ it('should export _getDefaultTranslationLocale function', () => {
39
+ expect(typeof _getDefaultTranslationLocale).toBe('function')
40
+ })
41
+ })
42
+
43
+ describe('getDefaultTranslationLocale', () => {
44
+ afterEach(() => {
45
+ // Restore original window
46
+ global.window = originalWindow
47
+ vi.clearAllMocks()
48
+ })
49
+
50
+ it('should return default locale in browser environment', () => {
51
+ // Mock browser environment
52
+ global.window = {
53
+ navigator: {
54
+ languages: ['fr', 'en'],
55
+ },
56
+ } as any
57
+
58
+ const locale = getDefaultTranslationLocale()
59
+ expect(locale).toBe('fr')
60
+ })
61
+
62
+ it('should use navigator.languages when available', () => {
63
+ // Mock browser environment with navigator.languages
64
+ global.window = {
65
+ navigator: {
66
+ languages: ['de', 'es', 'en'],
67
+ },
68
+ } as any
69
+
70
+ const locale = getDefaultTranslationLocale()
71
+ expect(locale).toBe('de')
72
+ })
73
+
74
+ it('should fallback to English when navigator.languages is null/undefined', () => {
75
+ // Mock browser environment with null languages
76
+ global.window = {
77
+ navigator: {
78
+ languages: null,
79
+ },
80
+ } as any
81
+
82
+ const locale = getDefaultTranslationLocale()
83
+ expect(locale).toBe('en')
84
+ })
85
+
86
+ it('should return English in server environment (no window)', () => {
87
+ // Mock server environment
88
+ delete (global as any).window
89
+
90
+ const locale = getDefaultTranslationLocale()
91
+ expect(locale).toBe('en')
92
+ })
93
+
94
+ it('should handle undefined navigator gracefully', () => {
95
+ // Mock browser environment without navigator
96
+ global.window = {
97
+ navigator: undefined,
98
+ } as any
99
+
100
+ const locale = getDefaultTranslationLocale()
101
+ expect(locale).toBe('en')
102
+ })
103
+
104
+ it('should handle empty languages array', () => {
105
+ // Mock browser environment with empty languages array
106
+ global.window = {
107
+ navigator: {
108
+ languages: [],
109
+ },
110
+ } as any
111
+
112
+ const locale = getDefaultTranslationLocale()
113
+ expect(locale).toBe('en')
114
+ })
115
+
116
+ it('should work with complex browser language preferences', () => {
117
+ // Test realistic browser scenario
118
+ global.window = {
119
+ navigator: {
120
+ languages: ['zh-TW', 'zh', 'en-US', 'en'],
121
+ },
122
+ } as any
123
+
124
+ const locale = getDefaultTranslationLocale()
125
+ expect(locale).toBe('zh-tw') // Should match zh-TW exactly
126
+ })
127
+ })
128
+
129
+ describe('_getDefaultTranslationLocale (internal logic)', () => {
130
+ const tests: DefaultLanguageTest[] = [
131
+ {
132
+ name: 'finds a matching language locale',
133
+ input: ['fr'],
134
+ output: 'fr',
135
+ },
136
+ {
137
+ name: 'finds a matching region locale',
138
+ input: ['pt-PT'],
139
+ output: 'pt-pt',
140
+ },
141
+ {
142
+ name: 'picks a region locale if no language locale available',
143
+ input: ['pt'],
144
+ output: 'pt-br',
145
+ },
146
+ {
147
+ name: 'picks a language locale if no region locale available',
148
+ input: ['fr-CA'],
149
+ output: 'fr',
150
+ },
151
+ {
152
+ name: 'picks the first language that loosely matches',
153
+ input: ['fr-CA', 'pt-PT'],
154
+ output: 'fr',
155
+ },
156
+ ]
157
+
158
+ for (const test of tests) {
159
+ it(test.name, () => {
160
+ expect(_getDefaultTranslationLocale(test.input)).toEqual(test.output)
161
+ })
162
+ }
163
+
164
+ it('should return "en" when no locales provided', () => {
165
+ expect(_getDefaultTranslationLocale([])).toBe('en')
166
+ })
167
+
168
+ it('should return "en" when no supported locales found', () => {
169
+ expect(_getDefaultTranslationLocale(['xyz', 'abc', 'unknown'])).toBe('en')
170
+ })
171
+
172
+ it('should handle case insensitive matching', () => {
173
+ expect(_getDefaultTranslationLocale(['FR'])).toBe('fr')
174
+ expect(_getDefaultTranslationLocale(['PT-BR'])).toBe('pt-br')
175
+ expect(_getDefaultTranslationLocale(['ZH-CN'])).toBe('zh-cn')
176
+ })
177
+
178
+ it('should handle mixed case and separators', () => {
179
+ expect(_getDefaultTranslationLocale(['pt_BR'])).toBe('pt-br')
180
+ expect(_getDefaultTranslationLocale(['zh_CN'])).toBe('zh-cn')
181
+ expect(_getDefaultTranslationLocale(['KO_KR'])).toBe('ko-kr')
182
+ })
183
+
184
+ it('should prioritize exact matches over fallbacks', () => {
185
+ expect(_getDefaultTranslationLocale(['pt-pt', 'pt'])).toBe('pt-pt')
186
+ expect(_getDefaultTranslationLocale(['zh-tw', 'zh'])).toBe('zh-tw')
187
+ })
188
+
189
+ it('should handle default region assignments correctly', () => {
190
+ // Chinese defaults to zh-cn
191
+ expect(_getDefaultTranslationLocale(['zh'])).toBe('zh-cn')
192
+ // Portuguese defaults to pt-br
193
+ expect(_getDefaultTranslationLocale(['pt'])).toBe('pt-br')
194
+ // Korean defaults to ko-kr
195
+ expect(_getDefaultTranslationLocale(['ko'])).toBe('ko-kr')
196
+ // Hindi defaults to hi-in
197
+ expect(_getDefaultTranslationLocale(['hi'])).toBe('hi-in')
198
+ })
199
+
200
+ it('should handle region-to-language fallback', () => {
201
+ // fr-CA should fallback to fr (if we only have generic French)
202
+ expect(_getDefaultTranslationLocale(['fr-CA'])).toBe('fr')
203
+ // es-MX should fallback to es
204
+ expect(_getDefaultTranslationLocale(['es-MX'])).toBe('es')
205
+ })
206
+
207
+ it('should respect locale priority order', () => {
208
+ // First supported locale should win
209
+ expect(_getDefaultTranslationLocale(['unsupported', 'fr', 'de'])).toBe('fr')
210
+ expect(_getDefaultTranslationLocale(['xyz', 'de', 'fr'])).toBe('de')
211
+ })
212
+
213
+ it('should handle complex regional scenarios', () => {
214
+ // zh-HK not supported, should fallback to default zh-cn (doesn't continue to next locale)
215
+ expect(_getDefaultTranslationLocale(['zh-HK', 'zh-TW', 'en'])).toBe('zh-cn')
216
+ // zh-SG not supported, should fallback to default zh-cn
217
+ expect(_getDefaultTranslationLocale(['zh-SG', 'zh-CN', 'en'])).toBe('zh-cn')
218
+ // Test exact match takes priority
219
+ expect(_getDefaultTranslationLocale(['zh-TW', 'zh-CN'])).toBe('zh-tw')
220
+ // Test fallback behavior
221
+ expect(_getDefaultTranslationLocale(['zh-HK', 'zh'])).toBe('zh-cn')
222
+ })
223
+
224
+ it('should work with single locale array', () => {
225
+ expect(_getDefaultTranslationLocale(['fr'])).toBe('fr')
226
+ expect(_getDefaultTranslationLocale(['unsupported'])).toBe('en')
227
+ })
228
+
229
+ it('should handle whitespace and malformed locales gracefully', () => {
230
+ // These should not crash but return fallback
231
+ expect(_getDefaultTranslationLocale([' '])).toBe('en')
232
+ expect(_getDefaultTranslationLocale([''])).toBe('en')
233
+ // Malformed locales should fallback to English when they can't be parsed properly
234
+ expect(_getDefaultTranslationLocale(['fr-'])).toBe('en')
235
+ // But valid partial matches should work
236
+ expect(_getDefaultTranslationLocale(['fr', 'en'])).toBe('fr')
237
+ })
238
+ })
239
+
240
+ describe('getSupportedLocale (internal function behavior)', () => {
241
+ // Note: getSupportedLocale is not exported, but we can test its behavior through _getDefaultTranslationLocale
242
+ it('should find exact locale matches', () => {
243
+ // These test the internal getSupportedLocale logic through the public API
244
+ expect(_getDefaultTranslationLocale(['en'])).toBe('en')
245
+ expect(_getDefaultTranslationLocale(['fr'])).toBe('fr')
246
+ expect(_getDefaultTranslationLocale(['pt-br'])).toBe('pt-br')
247
+ })
248
+
249
+ it('should handle case normalization in exact matches', () => {
250
+ expect(_getDefaultTranslationLocale(['EN'])).toBe('en')
251
+ expect(_getDefaultTranslationLocale(['PT-BR'])).toBe('pt-br')
252
+ expect(_getDefaultTranslationLocale(['ZH-TW'])).toBe('zh-tw')
253
+ })
254
+
255
+ it('should fall back from region to language', () => {
256
+ // fr-CA -> fr (assuming we have generic French)
257
+ expect(_getDefaultTranslationLocale(['fr-CA'])).toBe('fr')
258
+ // es-AR -> es
259
+ expect(_getDefaultTranslationLocale(['es-AR'])).toBe('es')
260
+ })
261
+
262
+ it('should assign default regions for base languages', () => {
263
+ // zh -> zh-cn (default Chinese region)
264
+ expect(_getDefaultTranslationLocale(['zh'])).toBe('zh-cn')
265
+ // pt -> pt-br (default Portuguese region)
266
+ expect(_getDefaultTranslationLocale(['pt'])).toBe('pt-br')
267
+ })
268
+
269
+ it('should return null for unsupported languages (via fallback to en)', () => {
270
+ // Unsupported languages should result in English fallback
271
+ expect(_getDefaultTranslationLocale(['xyz'])).toBe('en')
272
+ expect(_getDefaultTranslationLocale(['unsupported-locale'])).toBe('en')
273
+ })
274
+ })
275
+
276
+ describe('DEFAULT_LOCALE_REGIONS behavior', () => {
277
+ it('should apply Chinese default region correctly', () => {
278
+ expect(_getDefaultTranslationLocale(['zh'])).toBe('zh-cn')
279
+ })
280
+
281
+ it('should apply Portuguese default region correctly', () => {
282
+ expect(_getDefaultTranslationLocale(['pt'])).toBe('pt-br')
283
+ })
284
+
285
+ it('should apply Korean default region correctly', () => {
286
+ expect(_getDefaultTranslationLocale(['ko'])).toBe('ko-kr')
287
+ })
288
+
289
+ it('should apply Hindi default region correctly', () => {
290
+ expect(_getDefaultTranslationLocale(['hi'])).toBe('hi-in')
291
+ })
292
+
293
+ it('should not affect languages without default regions', () => {
294
+ // Languages like French, Spanish, German don't have default regions
295
+ expect(_getDefaultTranslationLocale(['fr'])).toBe('fr')
296
+ expect(_getDefaultTranslationLocale(['es'])).toBe('es')
297
+ expect(_getDefaultTranslationLocale(['de'])).toBe('de')
298
+ })
299
+ })
300
+
301
+ describe('TLLanguage type integration', () => {
302
+ it('should work correctly with TypeScript type system', () => {
303
+ // Test that return values match the TLLanguage locale type
304
+ const result: TLLanguage['locale'] = _getDefaultTranslationLocale(['fr'])
305
+ expect(result).toBe('fr')
306
+ })
307
+
308
+ it('should be compatible with LANGUAGES array', () => {
309
+ const allLocales: TLLanguage['locale'][] = LANGUAGES.map((lang) => lang.locale)
310
+
311
+ // Every result should be a valid locale from LANGUAGES
312
+ const testLocales = ['fr', 'pt-br', 'zh-cn', 'unsupported']
313
+ for (const locale of testLocales) {
314
+ const result = _getDefaultTranslationLocale([locale])
315
+ expect(allLocales).toContain(result)
316
+ }
317
+ })
318
+ })
319
+
320
+ describe('edge cases and robustness', () => {
321
+ it('should handle null and undefined inputs gracefully', () => {
322
+ // These should not crash the function
323
+ expect(() => _getDefaultTranslationLocale([] as any)).not.toThrow()
324
+ expect(_getDefaultTranslationLocale([])).toBe('en')
325
+ })
326
+
327
+ it('should handle very large locale arrays', () => {
328
+ const manyLocales = Array.from({ length: 1000 }, (_, i) => `lang-${i}`).concat(['fr']) // Add a valid one at the end
329
+
330
+ expect(_getDefaultTranslationLocale(manyLocales)).toBe('fr')
331
+ })
332
+
333
+ it('should handle special characters in locale strings', () => {
334
+ // Should not crash on malformed input
335
+ expect(() => _getDefaultTranslationLocale(['@#$%', 'fr'])).not.toThrow()
336
+ expect(_getDefaultTranslationLocale(['@#$%', 'fr'])).toBe('fr')
337
+ })
338
+
339
+ it('should handle extremely long locale strings', () => {
340
+ const longLocale = 'a'.repeat(1000)
341
+ expect(() => _getDefaultTranslationLocale([longLocale, 'fr'])).not.toThrow()
342
+ expect(_getDefaultTranslationLocale([longLocale, 'fr'])).toBe('fr')
343
+ })
344
+
345
+ it('should be consistent across multiple calls', () => {
346
+ const input = ['de', 'fr', 'es']
347
+ const result1 = _getDefaultTranslationLocale(input)
348
+ const result2 = _getDefaultTranslationLocale(input)
349
+ const result3 = _getDefaultTranslationLocale([...input]) // Spread to ensure different array
350
+
351
+ expect(result1).toBe(result2)
352
+ expect(result2).toBe(result3)
353
+ expect(result1).toBe('de')
354
+ })
355
+ })
356
+
357
+ describe('real-world usage scenarios', () => {
358
+ it('should handle typical browser language lists', () => {
359
+ // Common browser scenarios
360
+ expect(_getDefaultTranslationLocale(['en-US', 'en'])).toBe('en')
361
+ expect(_getDefaultTranslationLocale(['es-ES', 'es', 'en'])).toBe('es')
362
+ expect(_getDefaultTranslationLocale(['fr-FR', 'fr', 'en-US', 'en'])).toBe('fr')
363
+ })
364
+
365
+ it('should work for mobile browser scenarios', () => {
366
+ // iOS Safari typical format
367
+ expect(_getDefaultTranslationLocale(['zh-Hans-CN', 'zh-Hans', 'zh'])).toBe('zh-cn')
368
+ // Android Chrome typical format
369
+ expect(_getDefaultTranslationLocale(['pt-BR', 'pt', 'en-US'])).toBe('pt-br')
370
+ })
371
+
372
+ it('should handle multilingual user preferences', () => {
373
+ // User who speaks multiple languages
374
+ expect(_getDefaultTranslationLocale(['de-CH', 'fr-CH', 'it-CH', 'en'])).toBe('de')
375
+ expect(_getDefaultTranslationLocale(['es-MX', 'en-US', 'fr'])).toBe('es')
376
+ })
377
+
378
+ it('should work in internationalization scenarios', () => {
379
+ // Various international format inputs
380
+ expect(_getDefaultTranslationLocale(['ja-JP'])).toBe('ja')
381
+ expect(_getDefaultTranslationLocale(['ko-KR'])).toBe('ko-kr')
382
+ expect(_getDefaultTranslationLocale(['ar-SA'])).toBe('ar')
383
+ expect(_getDefaultTranslationLocale(['hi-IN'])).toBe('hi-in')
384
+ })
385
+ })
43
386
  })