metal-orm 1.0.100 → 1.0.102
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/index.cjs +57 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +57 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-entities/schema.mjs +4 -0
- package/scripts/inflection/pt-br.mjs +468 -445
- package/src/query-builder/relation-include-strategies.ts +35 -4
- package/src/query-builder/relation-join-planner.ts +73 -2
|
@@ -1,445 +1,468 @@
|
|
|
1
|
-
import {
|
|
2
|
-
applyToCompoundHead,
|
|
3
|
-
applyToCompoundWords,
|
|
4
|
-
detectTextFormat,
|
|
5
|
-
normalizeLookup,
|
|
6
|
-
splitIntoWords,
|
|
7
|
-
stripDiacritics
|
|
8
|
-
} from './compound.mjs';
|
|
9
|
-
|
|
10
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
-
// PATTERNS
|
|
12
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Precompiled regex patterns for performance.
|
|
16
|
-
* @type {Readonly<Record<string, RegExp>>}
|
|
17
|
-
*/
|
|
18
|
-
const PATTERNS = Object.freeze({
|
|
19
|
-
consonantEnding: /[rzn]$/,
|
|
20
|
-
consonantEsEnding: /[rzn]es$/,
|
|
21
|
-
endsInX: /x$/,
|
|
22
|
-
vowelBeforeS: /[aeiou]s$/,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
-
// IRREGULAR DICTIONARIES (all normalized - no diacritics)
|
|
27
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Default irregular plurals for Brazilian Portuguese.
|
|
31
|
-
* Keys AND values are normalized (no diacritics, lowercase).
|
|
32
|
-
* @type {Readonly<Record<string, string>>}
|
|
33
|
-
*/
|
|
34
|
-
export const PT_BR_DEFAULT_IRREGULARS = Object.freeze({
|
|
35
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
36
|
-
// -ão → -ães (irregular, must memorize)
|
|
37
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
38
|
-
'pao': 'paes',
|
|
39
|
-
'cao': 'caes',
|
|
40
|
-
'alemao': 'alemaes',
|
|
41
|
-
'capitao': 'capitaes',
|
|
42
|
-
'charlatao': 'charlataes',
|
|
43
|
-
'escrivao': 'escrivaes',
|
|
44
|
-
'tabeliao': 'tabeliaes',
|
|
45
|
-
'guardiao': 'guardiaes',
|
|
46
|
-
'sacristao': 'sacristaes',
|
|
47
|
-
|
|
48
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
49
|
-
// -ão → -ãos (irregular, must memorize)
|
|
50
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
51
|
-
'mao': 'maos',
|
|
52
|
-
'cidadao': 'cidadaos',
|
|
53
|
-
'cristao': 'cristaos',
|
|
54
|
-
'irmao': 'irmaos',
|
|
55
|
-
'orgao': 'orgaos',
|
|
56
|
-
'bencao': 'bencaos',
|
|
57
|
-
'grao': 'graos',
|
|
58
|
-
'orfao': 'orfaos',
|
|
59
|
-
'sotao': 'sotaos',
|
|
60
|
-
'acordao': 'acordaos',
|
|
61
|
-
'cortesao': 'cortesaos',
|
|
62
|
-
'pagao': 'pagaos',
|
|
63
|
-
'chao': 'chaos',
|
|
64
|
-
'vao': 'vaos',
|
|
65
|
-
|
|
66
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
67
|
-
// -l special cases
|
|
68
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
69
|
-
'mal': 'males',
|
|
70
|
-
'consul': 'consules',
|
|
71
|
-
|
|
72
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
73
|
-
// Unstressed -il → -eis (paroxytones)
|
|
74
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
75
|
-
'fossil': 'fosseis',
|
|
76
|
-
'reptil': 'repteis',
|
|
77
|
-
'facil': 'faceis',
|
|
78
|
-
'dificil': 'dificeis',
|
|
79
|
-
'util': 'uteis',
|
|
80
|
-
'inutil': 'inuteis',
|
|
81
|
-
'agil': 'ageis',
|
|
82
|
-
'fragil': 'frageis',
|
|
83
|
-
'projetil': 'projeteis',
|
|
84
|
-
'volatil': 'volateis',
|
|
85
|
-
'docil': 'doceis',
|
|
86
|
-
'portatil': 'portateis',
|
|
87
|
-
'textil': 'texteis',
|
|
88
|
-
|
|
89
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
90
|
-
//
|
|
91
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
92
|
-
'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
*
|
|
164
|
-
* @type {
|
|
165
|
-
*/
|
|
166
|
-
export const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
['
|
|
216
|
-
['
|
|
217
|
-
['
|
|
218
|
-
// -
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
['
|
|
222
|
-
['
|
|
223
|
-
['
|
|
224
|
-
['
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
*
|
|
267
|
-
* @
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
))
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
) => {
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1
|
+
import {
|
|
2
|
+
applyToCompoundHead,
|
|
3
|
+
applyToCompoundWords,
|
|
4
|
+
detectTextFormat,
|
|
5
|
+
normalizeLookup,
|
|
6
|
+
splitIntoWords,
|
|
7
|
+
stripDiacritics
|
|
8
|
+
} from './compound.mjs';
|
|
9
|
+
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// PATTERNS
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Precompiled regex patterns for performance.
|
|
16
|
+
* @type {Readonly<Record<string, RegExp>>}
|
|
17
|
+
*/
|
|
18
|
+
const PATTERNS = Object.freeze({
|
|
19
|
+
consonantEnding: /[rzn]$/,
|
|
20
|
+
consonantEsEnding: /[rzn]es$/,
|
|
21
|
+
endsInX: /x$/,
|
|
22
|
+
vowelBeforeS: /[aeiou]s$/,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
// IRREGULAR DICTIONARIES (all normalized - no diacritics)
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default irregular plurals for Brazilian Portuguese.
|
|
31
|
+
* Keys AND values are normalized (no diacritics, lowercase).
|
|
32
|
+
* @type {Readonly<Record<string, string>>}
|
|
33
|
+
*/
|
|
34
|
+
export const PT_BR_DEFAULT_IRREGULARS = Object.freeze({
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// -ão → -ães (irregular, must memorize)
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
38
|
+
'pao': 'paes',
|
|
39
|
+
'cao': 'caes',
|
|
40
|
+
'alemao': 'alemaes',
|
|
41
|
+
'capitao': 'capitaes',
|
|
42
|
+
'charlatao': 'charlataes',
|
|
43
|
+
'escrivao': 'escrivaes',
|
|
44
|
+
'tabeliao': 'tabeliaes',
|
|
45
|
+
'guardiao': 'guardiaes',
|
|
46
|
+
'sacristao': 'sacristaes',
|
|
47
|
+
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// -ão → -ãos (irregular, must memorize)
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
51
|
+
'mao': 'maos',
|
|
52
|
+
'cidadao': 'cidadaos',
|
|
53
|
+
'cristao': 'cristaos',
|
|
54
|
+
'irmao': 'irmaos',
|
|
55
|
+
'orgao': 'orgaos',
|
|
56
|
+
'bencao': 'bencaos',
|
|
57
|
+
'grao': 'graos',
|
|
58
|
+
'orfao': 'orfaos',
|
|
59
|
+
'sotao': 'sotaos',
|
|
60
|
+
'acordao': 'acordaos',
|
|
61
|
+
'cortesao': 'cortesaos',
|
|
62
|
+
'pagao': 'pagaos',
|
|
63
|
+
'chao': 'chaos',
|
|
64
|
+
'vao': 'vaos',
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// -l special cases
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
69
|
+
'mal': 'males',
|
|
70
|
+
'consul': 'consules',
|
|
71
|
+
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Unstressed -il → -eis (paroxytones)
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
75
|
+
'fossil': 'fosseis',
|
|
76
|
+
'reptil': 'repteis',
|
|
77
|
+
'facil': 'faceis',
|
|
78
|
+
'dificil': 'dificeis',
|
|
79
|
+
'util': 'uteis',
|
|
80
|
+
'inutil': 'inuteis',
|
|
81
|
+
'agil': 'ageis',
|
|
82
|
+
'fragil': 'frageis',
|
|
83
|
+
'projetil': 'projeteis',
|
|
84
|
+
'volatil': 'volateis',
|
|
85
|
+
'docil': 'doceis',
|
|
86
|
+
'portatil': 'portateis',
|
|
87
|
+
'textil': 'texteis',
|
|
88
|
+
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
90
|
+
// Singular words ending in -s/-us (not invariable, need -es plural)
|
|
91
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
92
|
+
'deus': 'deuses',
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
95
|
+
// Invariable words (paroxytone/proparoxytone ending in -s/-x)
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
97
|
+
'caos': 'caos',
|
|
98
|
+
'onibus': 'onibus',
|
|
99
|
+
'lapis': 'lapis',
|
|
100
|
+
'virus': 'virus',
|
|
101
|
+
'atlas': 'atlas',
|
|
102
|
+
'pires': 'pires',
|
|
103
|
+
'cais': 'cais',
|
|
104
|
+
'torax': 'torax',
|
|
105
|
+
'fenix': 'fenix',
|
|
106
|
+
'xerox': 'xerox',
|
|
107
|
+
'latex': 'latex',
|
|
108
|
+
'index': 'index',
|
|
109
|
+
'duplex': 'duplex',
|
|
110
|
+
'telex': 'telex',
|
|
111
|
+
'climax': 'climax',
|
|
112
|
+
'simples': 'simples',
|
|
113
|
+
'oasis': 'oasis',
|
|
114
|
+
'tenis': 'tenis',
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// -ês → -eses (nationalities, months, etc.)
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
119
|
+
'portugues': 'portugueses',
|
|
120
|
+
'ingles': 'ingleses',
|
|
121
|
+
'frances': 'franceses',
|
|
122
|
+
'holandes': 'holandeses',
|
|
123
|
+
'japones': 'japoneses',
|
|
124
|
+
'chines': 'chineses',
|
|
125
|
+
'irlandes': 'irlandeses',
|
|
126
|
+
'escoces': 'escoceses',
|
|
127
|
+
'mes': 'meses',
|
|
128
|
+
'burges': 'burgueses',
|
|
129
|
+
'fregues': 'fregueses',
|
|
130
|
+
'marques': 'marqueses',
|
|
131
|
+
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
133
|
+
// Other irregulars
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
135
|
+
'qualquer': 'quaisquer',
|
|
136
|
+
'carater': 'caracteres',
|
|
137
|
+
'junior': 'juniores',
|
|
138
|
+
'senior': 'seniores',
|
|
139
|
+
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
141
|
+
// Ambiguous normalized forms (context-dependent)
|
|
142
|
+
// "pais" (plural of pai) is invariable; "país" → "países" handled by rule
|
|
143
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
144
|
+
'pais': 'paises', // país → países (oxytone in -s)
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Builds reverse irregular mapping (plural → singular).
|
|
149
|
+
* @param {Record<string, string>} irregulars
|
|
150
|
+
* @returns {Record<string, string>}
|
|
151
|
+
*/
|
|
152
|
+
const buildSingularIrregulars = (irregulars) => {
|
|
153
|
+
const result = {};
|
|
154
|
+
for (const [singular, plural] of Object.entries(irregulars)) {
|
|
155
|
+
if (plural !== singular) {
|
|
156
|
+
result[plural] = singular;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Default irregular singulars (auto-generated reverse mapping).
|
|
164
|
+
* @type {Readonly<Record<string, string>>}
|
|
165
|
+
*/
|
|
166
|
+
export const PT_BR_DEFAULT_SINGULAR_IRREGULARS = Object.freeze(
|
|
167
|
+
buildSingularIrregulars(PT_BR_DEFAULT_IRREGULARS)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
171
|
+
// CONNECTORS
|
|
172
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Portuguese connector words used in compound expressions.
|
|
176
|
+
* @type {ReadonlySet<string>}
|
|
177
|
+
*/
|
|
178
|
+
export const PT_BR_CONNECTORS = Object.freeze(new Set(
|
|
179
|
+
[
|
|
180
|
+
'de', 'da', 'do', 'das', 'dos',
|
|
181
|
+
'em', 'na', 'no', 'nas', 'nos',
|
|
182
|
+
'a', 'ao', 'as', 'aos',
|
|
183
|
+
'com', 'sem', 'sob', 'sobre',
|
|
184
|
+
'para', 'por', 'pela', 'pelo', 'pelas', 'pelos',
|
|
185
|
+
'entre', 'contra', 'perante',
|
|
186
|
+
'e', 'ou'
|
|
187
|
+
].map(normalizeLookup)
|
|
188
|
+
));
|
|
189
|
+
|
|
190
|
+
const hasConnectorWord = (term, connectors) => {
|
|
191
|
+
if (!term || !String(term).trim()) return false;
|
|
192
|
+
const original = String(term).trim();
|
|
193
|
+
const format = detectTextFormat(original);
|
|
194
|
+
const words = splitIntoWords(original, format);
|
|
195
|
+
return words.some(word => connectors?.has?.(normalizeLookup(word)));
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
199
|
+
// INFLECTION RULES
|
|
200
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Pluralization rules for Portuguese words.
|
|
204
|
+
* Format: [suffix, replacement, suffixLength]
|
|
205
|
+
* @type {ReadonlyArray<Readonly<[string, string, number]>>}
|
|
206
|
+
*/
|
|
207
|
+
const PLURAL_RULES = Object.freeze([
|
|
208
|
+
// -ão → -ões (default; -ães and -ãos handled via irregulars)
|
|
209
|
+
['ao', 'oes', 2],
|
|
210
|
+
// -m → -ns
|
|
211
|
+
['m', 'ns', 1],
|
|
212
|
+
// -l endings
|
|
213
|
+
['al', 'ais', 2],
|
|
214
|
+
['el', 'eis', 2],
|
|
215
|
+
['ol', 'ois', 2],
|
|
216
|
+
['ul', 'uis', 2],
|
|
217
|
+
['il', 'is', 2], // Stressed -il; unstressed in irregulars
|
|
218
|
+
// Oxytone words ending in -s add -es (gás→gases, ás→ases, ês→eses, etc.)
|
|
219
|
+
// Note: normalized form (no diacritics), so "gás" becomes "gas"
|
|
220
|
+
['as', 'ases', 2],
|
|
221
|
+
['es', 'eses', 2],
|
|
222
|
+
['is', 'ises', 2],
|
|
223
|
+
['os', 'oses', 2],
|
|
224
|
+
['us', 'uses', 2],
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Singularization rules for Portuguese words.
|
|
229
|
+
* Format: [suffix, replacement, suffixLength]
|
|
230
|
+
* @type {ReadonlyArray<Readonly<[string, string, number]>>}
|
|
231
|
+
*/
|
|
232
|
+
const SINGULAR_RULES = Object.freeze([
|
|
233
|
+
// -ões/-ães/-ãos → -ão
|
|
234
|
+
['oes', 'ao', 3],
|
|
235
|
+
['aes', 'ao', 3],
|
|
236
|
+
['aos', 'ao', 3],
|
|
237
|
+
// -ns → -m
|
|
238
|
+
['ns', 'm', 2],
|
|
239
|
+
// -l endings reverse
|
|
240
|
+
['ais', 'al', 3],
|
|
241
|
+
['eis', 'el', 3],
|
|
242
|
+
['ois', 'ol', 3],
|
|
243
|
+
['uis', 'ul', 3],
|
|
244
|
+
['is', 'il', 2],
|
|
245
|
+
// Oxytone -Vses → -Vs (gases→gas, meses→mes, etc.)
|
|
246
|
+
['ases', 'as', 4],
|
|
247
|
+
['eses', 'es', 4],
|
|
248
|
+
['ises', 'is', 4],
|
|
249
|
+
['oses', 'os', 4],
|
|
250
|
+
['uses', 'us', 4],
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
254
|
+
// UTILITY FUNCTIONS
|
|
255
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Normalizes a word for rule matching and irregular lookup.
|
|
259
|
+
* @param {string} word - The word to normalize
|
|
260
|
+
* @returns {string} Normalized word (lowercase, no diacritics)
|
|
261
|
+
*/
|
|
262
|
+
const normalizeWord = (word) =>
|
|
263
|
+
stripDiacritics((word ?? '').toString()).toLowerCase().trim();
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Applies suffix rules to a word.
|
|
267
|
+
* @param {string} word - Normalized word
|
|
268
|
+
* @param {ReadonlyArray<Readonly<[string, string, number]>>} rules
|
|
269
|
+
* @returns {string|null} Transformed word or null if no rule matched
|
|
270
|
+
*/
|
|
271
|
+
const applyRules = (word, rules) => {
|
|
272
|
+
for (const [suffix, replacement, length] of rules) {
|
|
273
|
+
if (word.endsWith(suffix)) {
|
|
274
|
+
return word.slice(0, -length) + replacement;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
281
|
+
// PLURALIZATION
|
|
282
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Converts a Portuguese word to its plural form.
|
|
286
|
+
* Output is normalized (no diacritics, lowercase).
|
|
287
|
+
*
|
|
288
|
+
* @param {string} word - The word to pluralize
|
|
289
|
+
* @param {Record<string, string>} [irregulars=PT_BR_DEFAULT_IRREGULARS]
|
|
290
|
+
* @returns {string} The pluralized word (normalized)
|
|
291
|
+
*/
|
|
292
|
+
export const pluralizeWordPtBr = (
|
|
293
|
+
word,
|
|
294
|
+
irregulars = PT_BR_DEFAULT_IRREGULARS
|
|
295
|
+
) => {
|
|
296
|
+
const normalized = normalizeWord(word);
|
|
297
|
+
if (!normalized) return '';
|
|
298
|
+
|
|
299
|
+
// 1. Check irregulars first
|
|
300
|
+
const irregular = irregulars[normalized];
|
|
301
|
+
if (irregular !== undefined) {
|
|
302
|
+
return irregular;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 2. Apply suffix-based rules
|
|
306
|
+
const ruleResult = applyRules(normalized, PLURAL_RULES);
|
|
307
|
+
if (ruleResult !== null) {
|
|
308
|
+
return ruleResult;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 3. Words ending in -x are typically invariable
|
|
312
|
+
if (PATTERNS.endsInX.test(normalized)) {
|
|
313
|
+
return normalized;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 4. Consonants r, z, n require -es
|
|
317
|
+
if (PATTERNS.consonantEnding.test(normalized)) {
|
|
318
|
+
return normalized + 'es';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 5. Words ending in -s (invariable or already plural)
|
|
322
|
+
if (normalized.endsWith('s')) {
|
|
323
|
+
return normalized;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 6. Default: add -s
|
|
327
|
+
return normalized + 's';
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
331
|
+
// SINGULARIZATION
|
|
332
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Converts a Portuguese word to its singular form.
|
|
336
|
+
* Output is normalized (no diacritics, lowercase).
|
|
337
|
+
*
|
|
338
|
+
* @param {string} word - The word to singularize
|
|
339
|
+
* @param {Record<string, string>} [irregulars=PT_BR_DEFAULT_SINGULAR_IRREGULARS]
|
|
340
|
+
* @returns {string} The singularized word (normalized)
|
|
341
|
+
*/
|
|
342
|
+
export const singularizeWordPtBr = (
|
|
343
|
+
word,
|
|
344
|
+
irregulars = PT_BR_DEFAULT_SINGULAR_IRREGULARS
|
|
345
|
+
) => {
|
|
346
|
+
const normalized = normalizeWord(word);
|
|
347
|
+
if (!normalized) return '';
|
|
348
|
+
|
|
349
|
+
// 1. Check irregulars first
|
|
350
|
+
const irregular = irregulars[normalized];
|
|
351
|
+
if (irregular !== undefined) {
|
|
352
|
+
return irregular;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 2. Apply suffix-based rules
|
|
356
|
+
const ruleResult = applyRules(normalized, SINGULAR_RULES);
|
|
357
|
+
if (ruleResult !== null) {
|
|
358
|
+
return ruleResult;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 3. Handle consonant + es pattern
|
|
362
|
+
if (PATTERNS.consonantEsEnding.test(normalized)) {
|
|
363
|
+
return normalized.slice(0, -2);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 4. Words ending in vowel+s: remove s
|
|
367
|
+
if (PATTERNS.vowelBeforeS.test(normalized)) {
|
|
368
|
+
return normalized.slice(0, -1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 5. Already singular or invariable
|
|
372
|
+
return normalized;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
376
|
+
// NOUN SPECIFIERS (SUBSTANTIVOS DETERMINANTES)
|
|
377
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Portuguese words that act as specifiers/delimiters in compound nouns.
|
|
381
|
+
* When these appear as the second term, only the first term varies.
|
|
382
|
+
* @type {ReadonlySet<string>}
|
|
383
|
+
*/
|
|
384
|
+
export const PT_BR_NOUN_SPECIFIERS = Object.freeze(new Set(
|
|
385
|
+
[
|
|
386
|
+
'correcao', 'padrao', 'limite', 'chave', 'base', 'chefe',
|
|
387
|
+
'satelite', 'fantasma', 'monstro', 'escola', 'piloto',
|
|
388
|
+
'femea', 'macho', 'geral', 'solicitacao'
|
|
389
|
+
].map(normalizeLookup)
|
|
390
|
+
));
|
|
391
|
+
|
|
392
|
+
const isCompoundWithSpecifier = (term, specifiers = PT_BR_NOUN_SPECIFIERS) => {
|
|
393
|
+
if (!term || !String(term).trim()) return false;
|
|
394
|
+
const original = String(term).trim();
|
|
395
|
+
const format = detectTextFormat(original);
|
|
396
|
+
const words = splitIntoWords(original, format);
|
|
397
|
+
|
|
398
|
+
if (words.length < 2) return false;
|
|
399
|
+
|
|
400
|
+
// Check if the last word is a known specifier
|
|
401
|
+
const lastWord = words[words.length - 1];
|
|
402
|
+
return specifiers.has(normalizeLookup(lastWord));
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
406
|
+
// COMPOUND TERM HANDLING
|
|
407
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Pluralizes a compound property/relation name in Portuguese.
|
|
411
|
+
*/
|
|
412
|
+
export const pluralizeRelationPropertyPtBr = (
|
|
413
|
+
term,
|
|
414
|
+
{ pluralizeWord = pluralizeWordPtBr, connectors = PT_BR_CONNECTORS, specifiers = PT_BR_NOUN_SPECIFIERS } = {}
|
|
415
|
+
) => {
|
|
416
|
+
if (hasConnectorWord(term, connectors) || isCompoundWithSpecifier(term, specifiers)) {
|
|
417
|
+
return applyToCompoundHead(term, { connectors, transformWord: pluralizeWord });
|
|
418
|
+
}
|
|
419
|
+
return applyToCompoundWords(term, { connectors, transformWord: pluralizeWord });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Singularizes a compound property/relation name in Portuguese.
|
|
424
|
+
*/
|
|
425
|
+
export const singularizeRelationPropertyPtBr = (
|
|
426
|
+
term,
|
|
427
|
+
{ singularizeWord = singularizeWordPtBr, connectors = PT_BR_CONNECTORS, specifiers = PT_BR_NOUN_SPECIFIERS } = {}
|
|
428
|
+
) => {
|
|
429
|
+
if (hasConnectorWord(term, connectors) || isCompoundWithSpecifier(term, specifiers)) {
|
|
430
|
+
return applyToCompoundHead(term, { connectors, transformWord: singularizeWord });
|
|
431
|
+
}
|
|
432
|
+
return applyToCompoundWords(term, { connectors, transformWord: singularizeWord });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
436
|
+
// INFLECTOR FACTORY
|
|
437
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Creates a Brazilian Portuguese inflector instance.
|
|
441
|
+
*/
|
|
442
|
+
export const createPtBrInflector = ({ customIrregulars = {} } = {}) => {
|
|
443
|
+
const irregularPlurals = Object.freeze({
|
|
444
|
+
...PT_BR_DEFAULT_IRREGULARS,
|
|
445
|
+
...customIrregulars
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const irregularSingulars = Object.freeze({
|
|
449
|
+
...PT_BR_DEFAULT_SINGULAR_IRREGULARS,
|
|
450
|
+
...buildSingularIrregulars(customIrregulars)
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const pluralizeWord = (w) => pluralizeWordPtBr(w, irregularPlurals);
|
|
454
|
+
const singularizeWord = (w) => singularizeWordPtBr(w, irregularSingulars);
|
|
455
|
+
|
|
456
|
+
return Object.freeze({
|
|
457
|
+
locale: 'pt-BR',
|
|
458
|
+
irregularPlurals,
|
|
459
|
+
irregularSingulars,
|
|
460
|
+
pluralizeWord,
|
|
461
|
+
singularizeWord,
|
|
462
|
+
pluralizeRelationProperty: (term) => pluralizeRelationPropertyPtBr(term, { pluralizeWord }),
|
|
463
|
+
singularizeRelationProperty: (term) => singularizeRelationPropertyPtBr(term, { singularizeWord }),
|
|
464
|
+
normalizeForLookup: normalizeWord
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export default createPtBrInflector;
|