diff --git a/.gitignore b/.gitignore index b2533d2..0aea038 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ npm-debug.log* package-lock.json /dist /coverage +.yalc +.yalc/* +yalc.lock diff --git a/package.json b/package.json index b63e24d..3c3eb8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sysvale/show", - "version": "1.19.0", + "version": "1.20.0", "description": "A set of components used at Sysvale", "repository": { "type": "git", diff --git a/src/components/index.ts b/src/components/index.ts index aff42dc..3b3b308 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -16,61 +16,63 @@ import { useDialog } from '@/composables/useDialog'; /** Utils */ import { - convertKeysToCamelCase, - convertKeysToSnakeCase, - removeAccents, - generateKey, - toThousands, - setupVeeValidateWrapper, + convertKeysToCamelCase, + convertKeysToSnakeCase, + removeAccents, + generateKey, + toThousands, + setupVeeValidateWrapper, + pluralize, + pluralizeWithCount, + pluralizeWords, } from '../utils'; /* -------*/ -export { useRequest, useDialog }; +export { useRequest, useDialog, pluralize, pluralizeWithCount, pluralizeWords }; export default { - install(app: any, options = { - veeValidateOptions: {}, - disabledFeatures: [], - }) { - const { veeValidateOptions, disabledFeatures } = options; - const veeValidateWrapper = setupVeeValidateWrapper(veeValidateOptions); + install(app: any, options = { + veeValidateOptions: {}, + disabledFeatures: [], + }) { + const { veeValidateOptions, disabledFeatures } = options; + const veeValidateWrapper = setupVeeValidateWrapper(veeValidateOptions); - if (disabledFeatures.length > 0) { - app.provide('disabledFeatures', options.disabledFeatures); - } + if (disabledFeatures.length > 0) { + app.provide('disabledFeatures', options.disabledFeatures); + } - app.component('ShowRequestProvider', RequestProvider); - app.component('ShowRequestObserver', RequestObserver); - app.component('ShowRequestSelect', RequestSelect); - app.component('ShowForm', Form); - app.component('ShowField', Field); - app.component('ShowFieldArray', FieldArray); - app.component('ShowFormWizard', FormWizard); - app.component('ShowFeatureWrapper', FeatureWrapper); + app.component('ShowRequestProvider', RequestProvider); + app.component('ShowRequestObserver', RequestObserver); + app.component('ShowRequestSelect', RequestSelect); + app.component('ShowForm', Form); + app.component('ShowField', Field); + app.component('ShowFieldArray', FieldArray); + app.component('ShowFormWizard', FormWizard); + app.component('ShowFeatureWrapper', FeatureWrapper); + const utils = { + $showConvertKeysToCamelCase: convertKeysToCamelCase, + $showConvertKeysToSnakeCase: convertKeysToSnakeCase, + $showRemoveAccents: removeAccents, + $showGenerateKey: generateKey, + $showToThousands: toThousands, + }; - const utils = { - $showConvertKeysToCamelCase: convertKeysToCamelCase, - $showConvertKeysToSnakeCase: convertKeysToSnakeCase, - $showRemoveAccents: removeAccents, - $showGenerateKey: generateKey, - $showToThousands: toThousands, - }; + const version = Number(app.version.split('.')[0]); + + if (version <= 2) { + throw new Error('Essa versão só é compatível com projetos que possuem o Vue 3. Para projetos com a Vue 2, utilize a versão 0.3.0 ou inferior'); + } + + if (version > 2) { + // ficará disponível apenas com o uso do Options API + Object.keys(utils).forEach((key: string) => { + app.config.globalProperties[key] = utils[key]; + }); - const version = Number(app.version.split('.')[0]); - - if (version <= 2) { - throw new Error('Essa versão só é compatível com projetos que possuem o Vue 3. Para projetos com a Vue 2, utilize a versão 0.3.0 ou inferior'); - } - - if (version > 2) { - // ficará disponível apenas com o uso do Options API - Object.keys(utils).forEach((key: string) => { - app.config.globalProperties[key] = utils[key]; - }); - - // wrapper do vee-validate - app.config.globalProperties.$showVeeValidate = veeValidateWrapper; - } - }, -} \ No newline at end of file + // wrapper do vee-validate + app.config.globalProperties.$showVeeValidate = veeValidateWrapper; + } + }, +} diff --git a/src/utils/index.js b/src/utils/index.js index bdd4a63..b0681db 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -4,4 +4,5 @@ export { default as getFirstErrorMessage } from './getFirstErrorMessage'; export { default as removeAccents } from './removeAccents'; export { default as generateKey } from './generateKey'; export { default as toThousands } from './toThousands'; +export { pluralize, pluralizeWithCount, pluralizeWords } from './pluralize'; export { default as setupVeeValidateWrapper } from './setupVeeValidateWrapper'; diff --git a/src/utils/pluralize.ts b/src/utils/pluralize.ts new file mode 100644 index 0000000..1329053 --- /dev/null +++ b/src/utils/pluralize.ts @@ -0,0 +1,87 @@ +const irregulars: Record = { + pão: 'pães', + mão: 'mãos', + irmão: 'irmãos', + capitão: 'capitães', + cidadão: 'cidadãos', + cão: 'cães', + alemão: 'alemães', + mal: 'males', + órgão: 'órgãos', + nível: 'níveis' +}; + +export function pluralize( + count: number | string | null = null, + word: string, + customPlural: string = '', + customIrregulars: Record = {} +): string { + + let innerIrregulars = { + ...irregulars, + ...customIrregulars + } + + if (typeof count === 'string') { + word = count; + count = null; + } + + const absCount = Math.abs(count ?? 2); + + // Quantidades insuficientes para aplicação do plural + if (absCount < 2) return word; + + // Sufixo personalizado fornecido por quem chama a função + if (customPlural) return customPlural; + + // Cenário em que a palavra tem plural irregular + if (innerIrregulars[word]) return innerIrregulars[word]; + + // Palavras invariáveis terminadas em s ou x + if (word.endsWith('s') || word.endsWith('x')) return word; + + // Exceções do -ão (padrão geral: -ões) + if (word.endsWith('ão')) return word.slice(0, -2) + 'ões'; + + // -m -> -ns + if (word.endsWith('m')) return word.slice(0, -1) + 'ns'; + + // -r, -z -> +es + if (word.endsWith('r') || word.endsWith('z')) return word + 'es'; + + // -ol → -óis + if (word.endsWith('ol')) return word.slice(0, -2) + 'óis'; + + // -al, -ul → -ais, -uis + if (/[au]l$/.test(word)) return word.slice(0, -1) + 'is'; + + // -el → -éis + if (word.endsWith('el')) return word.slice(0, -2) + 'éis'; + + // -il → -is (oxítonas) [simplificado] + if (word.endsWith('il')) return word.slice(0, -2) + 'is'; + + // Regra geral para vogais finais + if (/[aeiou]$/.test(word)) return word + 's'; + + //Fallback + return word; +} + +export function pluralizeWithCount(...args: Parameters): string { + const [count] = args; + return `${count} ${pluralize(...args)}`; +} + +export function pluralizeWords( + count: number | string | null, + words: string[], + customPlural: string[] | string = '', + customIrregulars: Record = {} +): string { + let pluralizedWords = words.map((word, index) => pluralize(count, word, customPlural[index] ?? customPlural, customIrregulars)); + + return `${pluralizedWords.join(' ')}`; +} diff --git a/tests/utils/pluralize.test.ts b/tests/utils/pluralize.test.ts new file mode 100644 index 0000000..b3ba50c --- /dev/null +++ b/tests/utils/pluralize.test.ts @@ -0,0 +1,115 @@ +import { describe, test, expect } from 'vitest'; +import { pluralize, pluralizeWords, pluralizeWithCount } from '../../src/utils/pluralize'; // Adjust the path + +describe('pluralize()', () => { + test('returns same word if count is 1', () => { + expect(pluralize(1, 'carro')).toBe('carro'); + }); + + test('adds "s" for regular words ending in vowel', () => { + expect(pluralize(2, 'banana')).toBe('bananas'); + }); + + test('uses custom plural if provided', () => { + expect(pluralize(2, 'livro')).not.toBe('livrozinhos'); + expect(pluralize(2, 'livro', 'livrozinhos')).toBe('livrozinhos'); + }); + + test('does not change words ending in s or x', () => { + expect(pluralize(3, 'tórax')).toBe('tórax'); + expect(pluralize(3, 'lápis')).toBe('lápis'); + }); + + test('converts -ão to -ões', () => { + expect(pluralize(2, 'avião')).toBe('aviões'); + }); + + test('converts some irregulars properly', () => { + expect(pluralize(2, 'pão')).toBe('pães'); + expect(pluralize(2, 'alemão')).toBe('alemães'); + }); + + test('convert custom irregulars properly', () => { + expect(pluralize(2, 'órgão')).toBe('órgãos'); + expect(pluralize(2, 'cônsul')).not.toBe('cônsules'); + expect(pluralize(2, 'cônsul', undefined, { cônsul: 'cônsules'})).toBe('cônsules'); + expect(pluralize(2, 'maçã', undefined, { maçã: 'maçãs'})).toBe('maçãs'); + }); + + test('converts -m to -ns', () => { + expect(pluralize(2, 'homem')).toBe('homens'); + }); + + test('adds "es" for words ending in r or z', () => { + expect(pluralize(2, 'luz')).toBe('luzes'); + expect(pluralize(2, 'motor')).toBe('motores'); + }); + + test('converts-ol to -óis', () => { + expect(pluralize(2, 'sol')).toBe('sóis'); + }); + + test('converts -al, -ul to -ais, -uis', () => { + expect(pluralize(2, 'animal')).toBe('animais'); + expect(pluralize(2, 'azul')).toBe('azuis'); + }); + + test('converts -el to -éis', () => { + expect(pluralize(2, 'papel')).toBe('papéis'); + }); + + test('converts -il to -is', () => { + expect(pluralize(2, 'fuzil')).toBe('fuzis'); + }); + + test('uses plural when count is negative', () => { + expect(pluralize(-3, 'avião')).toBe('aviões'); + }); + + test('does not use plural when count is zero', () => { + expect(pluralize(0, 'mapa')).toBe('mapa'); + }); + + test('defaults count to 2 if null or undefined', () => { + expect(pluralize(null, 'livro')).toBe('livros'); + }); + + test('treats first param as word if count is a string', () => { + expect(pluralize('carro', '')).toBe('carros'); + }); +}); + +describe('pluralizeWithCount()', () => { + test('includes the count in the result', () => { + expect(pluralizeWithCount(1, 'carro')).toBe('1 carro'); + expect(pluralizeWithCount(2, 'carro')).toBe('2 carros'); + }); + + test('works with custom plural', () => { + expect(pluralizeWithCount(2, 'livro', 'livrozinhos')).toBe('2 livrozinhos'); + }); +}); + +describe('pluralizeWords()', () => { + test('works with one word', () => { + expect(pluralizeWords(0, ['papel'])).toBe('papel'); + expect(pluralizeWords(1, ['papel'])).toBe('papel'); + expect(pluralizeWords(2, ['papel'])).toBe('papéis'); + }); + + test('works with two words', () => { + expect(pluralizeWords(0, ['avião', 'antigo'])).toBe('avião antigo'); + expect(pluralizeWords(1, ['avião', 'antigo'])).toBe('avião antigo'); + expect(pluralizeWords(2, ['avião', 'antigo'])).toBe('aviões antigos'); + }); + + test('works with three words', () => { + expect(pluralizeWords(0, ['moto', 'branca', 'escolhida'])).toBe('moto branca escolhida'); + expect(pluralizeWords(1, ['moto', 'branca', 'escolhida'])).toBe('moto branca escolhida'); + expect(pluralizeWords(2, ['moto', 'branca', 'escolhida'])).toBe('motos brancas escolhidas'); + }); + + test('works with custom plurals', () => { + expect(pluralizeWords(2, ['o', 'avião', 'velho'], ['os', 'aviõezinhos', 'velhos'])).toBe('os aviõezinhos velhos'); + }); +});