metal-orm 1.1.10 → 1.1.16

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 (192) hide show
  1. package/README.md +758 -769
  2. package/dist/index.cjs +106 -43
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +5 -5
  5. package/dist/index.d.ts +5 -5
  6. package/dist/index.js +106 -43
  7. package/dist/index.js.map +1 -1
  8. package/package.json +100 -96
  9. package/scripts/generate-entities/render.mjs +21 -12
  10. package/scripts/generate-entities/schema.mjs +209 -195
  11. package/scripts/generate-entities/tree-detection.mjs +159 -153
  12. package/scripts/inflection/pt-br.mjs +468 -468
  13. package/scripts/run-eslint.mjs +33 -34
  14. package/scripts/show-sql-seed.sql +102 -0
  15. package/scripts/show-sql.mjs +321 -325
  16. package/src/bulk/bulk-delete-executor.ts +3 -5
  17. package/src/bulk/bulk-insert-executor.ts +7 -7
  18. package/src/bulk/bulk-update-executor.ts +2 -2
  19. package/src/bulk/bulk-upsert-executor.ts +5 -7
  20. package/src/codegen/naming-strategy.ts +54 -54
  21. package/src/codegen/typescript.ts +482 -482
  22. package/src/core/ast/adapters.ts +1 -1
  23. package/src/core/ast/builders.ts +123 -123
  24. package/src/core/ast/expression-builders.ts +745 -745
  25. package/src/core/ast/expression-nodes.ts +304 -304
  26. package/src/core/ast/expression-visitor.ts +212 -212
  27. package/src/core/ast/expression.ts +20 -20
  28. package/src/core/ast/helpers.ts +14 -14
  29. package/src/core/ast/join.ts +18 -18
  30. package/src/core/ast/query.ts +200 -200
  31. package/src/core/ddl/dialects/base-schema-dialect.ts +1 -1
  32. package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -1
  33. package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -1
  34. package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -1
  35. package/src/core/ddl/dialects/render-reference.test.ts +70 -70
  36. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -1
  37. package/src/core/ddl/introspect/catalogs/index.ts +5 -5
  38. package/src/core/ddl/introspect/catalogs/postgres.ts +146 -146
  39. package/src/core/ddl/introspect/context.ts +15 -15
  40. package/src/core/ddl/introspect/functions/postgres.ts +35 -35
  41. package/src/core/ddl/introspect/mysql.ts +113 -36
  42. package/src/core/ddl/introspect/registry.ts +40 -40
  43. package/src/core/ddl/introspect/run-select.ts +35 -35
  44. package/src/core/ddl/introspect/utils.ts +56 -56
  45. package/src/core/ddl/naming-strategy.ts +16 -16
  46. package/src/core/ddl/schema-dialect.ts +56 -56
  47. package/src/core/ddl/schema-diff.ts +234 -234
  48. package/src/core/ddl/schema-generator.ts +1 -1
  49. package/src/core/ddl/schema-introspect.ts +25 -25
  50. package/src/core/ddl/sql-writing.ts +171 -171
  51. package/src/core/dialect/abstract.ts +541 -541
  52. package/src/core/dialect/base/cte-compiler.ts +33 -33
  53. package/src/core/dialect/base/function-table-formatter.ts +103 -103
  54. package/src/core/dialect/base/groupby-compiler.ts +21 -21
  55. package/src/core/dialect/base/join-compiler.ts +25 -25
  56. package/src/core/dialect/base/orderby-compiler.ts +35 -35
  57. package/src/core/dialect/base/pagination-strategy.ts +32 -32
  58. package/src/core/dialect/base/returning-strategy.ts +57 -57
  59. package/src/core/dialect/base/sql-dialect.ts +294 -294
  60. package/src/core/dialect/dialect-factory.ts +91 -91
  61. package/src/core/dialect/mssql/functions.ts +120 -120
  62. package/src/core/dialect/mssql/index.ts +317 -317
  63. package/src/core/dialect/mysql/functions.ts +103 -103
  64. package/src/core/dialect/mysql/index.ts +105 -105
  65. package/src/core/dialect/postgres/functions.ts +108 -108
  66. package/src/core/dialect/postgres/index.ts +91 -91
  67. package/src/core/dialect/sqlite/functions.ts +129 -129
  68. package/src/core/dialect/sqlite/index.ts +67 -67
  69. package/src/core/driver/database-driver.ts +19 -19
  70. package/src/core/driver/mssql-driver.ts +23 -23
  71. package/src/core/driver/mysql-driver.ts +23 -23
  72. package/src/core/driver/postgres-driver.ts +23 -23
  73. package/src/core/driver/sqlite-driver.ts +23 -23
  74. package/src/core/execution/db-executor.ts +92 -92
  75. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -94
  76. package/src/core/execution/executors/mssql-executor.ts +184 -184
  77. package/src/core/execution/executors/mysql-executor.ts +88 -88
  78. package/src/core/execution/executors/postgres-executor.ts +26 -26
  79. package/src/core/execution/executors/sqlite-executor.ts +42 -42
  80. package/src/core/execution/pooling/pool-types.ts +30 -30
  81. package/src/core/execution/pooling/pool.ts +275 -275
  82. package/src/core/functions/array.ts +35 -35
  83. package/src/core/functions/control-flow.ts +79 -79
  84. package/src/core/functions/datetime.ts +237 -237
  85. package/src/core/functions/json.ts +70 -70
  86. package/src/core/functions/numeric.ts +297 -297
  87. package/src/core/functions/text.ts +334 -334
  88. package/src/core/functions/types.ts +33 -33
  89. package/src/core/hydration/types.ts +51 -51
  90. package/src/core/sql/sql-operator-config.ts +39 -39
  91. package/src/core/sql/sql.ts +152 -152
  92. package/src/decorators/column-decorator.ts +87 -87
  93. package/src/decorators/decorator-metadata.ts +143 -143
  94. package/src/decorators/entity.ts +93 -93
  95. package/src/decorators/index.ts +39 -39
  96. package/src/decorators/relations.ts +231 -231
  97. package/src/decorators/transformers/built-in/string-transformers.ts +231 -231
  98. package/src/decorators/transformers/transformer-decorators.ts +134 -134
  99. package/src/decorators/transformers/transformer-executor.ts +171 -171
  100. package/src/decorators/transformers/transformer-metadata.ts +73 -73
  101. package/src/decorators/validators/built-in/br-cep-validator.ts +55 -55
  102. package/src/decorators/validators/built-in/br-cnpj-validator.ts +105 -105
  103. package/src/decorators/validators/built-in/br-cpf-validator.ts +103 -103
  104. package/src/decorators/validators/country-validator-registry.ts +64 -64
  105. package/src/decorators/validators/country-validators-decorators.ts +203 -203
  106. package/src/decorators/validators/country-validators.ts +105 -105
  107. package/src/decorators/validators/validator-metadata.ts +59 -59
  108. package/src/dto/dto-types.ts +226 -226
  109. package/src/dto/filter-types.ts +141 -141
  110. package/src/dto/index.ts +95 -95
  111. package/src/dto/openapi/generators/base.ts +29 -29
  112. package/src/dto/openapi/generators/column.ts +37 -37
  113. package/src/dto/openapi/generators/dto.ts +91 -91
  114. package/src/dto/openapi/generators/filter.ts +75 -75
  115. package/src/dto/openapi/generators/nested-dto.ts +618 -618
  116. package/src/dto/openapi/generators/pagination.ts +111 -111
  117. package/src/dto/openapi/generators/relation-filter.ts +228 -228
  118. package/src/dto/openapi/index.ts +12 -12
  119. package/src/dto/openapi/types.ts +101 -101
  120. package/src/dto/openapi/utilities.ts +95 -95
  121. package/src/dto/pagination-utils.ts +150 -150
  122. package/src/dto/transform.ts +197 -197
  123. package/src/index.ts +86 -86
  124. package/src/orm/als.ts +58 -58
  125. package/src/orm/domain-event-bus.ts +119 -119
  126. package/src/orm/entity-context.ts +86 -86
  127. package/src/orm/entity-materializer.ts +159 -159
  128. package/src/orm/entity-meta.ts +97 -97
  129. package/src/orm/entity-registry.ts +39 -39
  130. package/src/orm/entity-relation-cache.ts +39 -39
  131. package/src/orm/execute.ts +194 -194
  132. package/src/orm/execution-context.ts +18 -18
  133. package/src/orm/hydration-context.ts +26 -26
  134. package/src/orm/hydration.ts +112 -112
  135. package/src/orm/identity-map.ts +61 -61
  136. package/src/orm/interceptor-pipeline.ts +16 -16
  137. package/src/orm/lazy-batch/belongs-to-many.ts +119 -119
  138. package/src/orm/lazy-batch/belongs-to.ts +108 -108
  139. package/src/orm/lazy-batch/has-many.ts +69 -69
  140. package/src/orm/lazy-batch/has-one.ts +68 -68
  141. package/src/orm/lazy-batch/shared.ts +125 -125
  142. package/src/orm/orm-session.ts +620 -620
  143. package/src/orm/orm.ts +34 -34
  144. package/src/orm/pooled-executor-factory.ts +121 -121
  145. package/src/orm/query-logger.ts +40 -40
  146. package/src/orm/relation-change-processor.ts +420 -420
  147. package/src/orm/relations/belongs-to.ts +143 -143
  148. package/src/orm/relations/has-many.ts +204 -204
  149. package/src/orm/relations/has-one.ts +174 -174
  150. package/src/orm/relations/many-to-many.ts +288 -288
  151. package/src/orm/relations/morph-many.ts +156 -156
  152. package/src/orm/relations/morph-one.ts +151 -151
  153. package/src/orm/relations/morph-to.ts +162 -162
  154. package/src/orm/runtime-types.ts +92 -92
  155. package/src/orm/save-graph.ts +561 -561
  156. package/src/orm/transaction-runner.ts +24 -24
  157. package/src/orm/unit-of-work.ts +426 -426
  158. package/src/query/index.ts +69 -69
  159. package/src/query/target.ts +46 -46
  160. package/src/query-builder/column-selector.ts +81 -81
  161. package/src/query-builder/delete.ts +141 -141
  162. package/src/query-builder/insert-query-state.ts +149 -149
  163. package/src/query-builder/insert.ts +117 -117
  164. package/src/query-builder/query-ast-service.ts +287 -287
  165. package/src/query-builder/query-resolution.ts +78 -78
  166. package/src/query-builder/raw-column-parser.ts +38 -38
  167. package/src/query-builder/relation-filter-utils.ts +153 -153
  168. package/src/query-builder/relation-join-planner.ts +10 -10
  169. package/src/query-builder/relation-manager.ts +87 -87
  170. package/src/query-builder/relation-projection-helper.ts +102 -102
  171. package/src/query-builder/relation-types.ts +9 -9
  172. package/src/query-builder/relation-utils.ts +15 -15
  173. package/src/query-builder/select/cte-facet.ts +40 -40
  174. package/src/query-builder/select/from-facet.ts +80 -80
  175. package/src/query-builder/select/join-facet.ts +62 -62
  176. package/src/query-builder/select/predicate-facet.ts +104 -104
  177. package/src/query-builder/select/projection-facet.ts +70 -70
  178. package/src/query-builder/select/relation-facet.ts +81 -81
  179. package/src/query-builder/select/setop-facet.ts +36 -36
  180. package/src/query-builder/select-helpers.ts +1 -1
  181. package/src/query-builder/select-query-builder-deps.ts +130 -130
  182. package/src/query-builder/select-query-state.ts +207 -207
  183. package/src/query-builder/update.ts +151 -151
  184. package/src/schema/column-types.ts +322 -322
  185. package/src/schema/table-guards.ts +37 -37
  186. package/src/schema/table.ts +281 -281
  187. package/src/tree/index.ts +13 -13
  188. package/src/tree/nested-set-strategy.ts +545 -545
  189. package/src/tree/tree-decorator.ts +397 -397
  190. package/src/tree/tree-manager.ts +754 -754
  191. package/src/tree/tree-query.ts +427 -427
  192. package/src/tree/tree-types.ts +299 -299
@@ -1,468 +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
- // 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;
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;