chore: Revert pagination formatting and pluralization (#12954)
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useNumberFormatter } from 'shared/composables/useNumberFormatter';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
@@ -25,7 +24,6 @@ const props = defineProps({
|
||||
});
|
||||
const emit = defineEmits(['update:currentPage']);
|
||||
const { t } = useI18n();
|
||||
const { formatCompactNumber, formatFullNumber } = useNumberFormatter();
|
||||
|
||||
const totalPages = computed(() =>
|
||||
Math.ceil(props.totalItems / props.itemsPerPage)
|
||||
@@ -45,27 +43,21 @@ const changePage = newPage => {
|
||||
};
|
||||
|
||||
const currentPageInformation = computed(() => {
|
||||
const translationKey = props.currentPageInfo || 'PAGINATION_FOOTER.SHOWING';
|
||||
return t(
|
||||
translationKey,
|
||||
props.currentPageInfo ? props.currentPageInfo : 'PAGINATION_FOOTER.SHOWING',
|
||||
{
|
||||
startItem: formatFullNumber(startItem.value),
|
||||
endItem: formatFullNumber(endItem.value),
|
||||
totalItems: formatCompactNumber(props.totalItems),
|
||||
},
|
||||
Number(props.totalItems)
|
||||
startItem: startItem.value,
|
||||
endItem: endItem.value,
|
||||
totalItems: props.totalItems,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const pageInfo = computed(() => {
|
||||
return t(
|
||||
'PAGINATION_FOOTER.CURRENT_PAGE_INFO',
|
||||
{
|
||||
currentPage: '',
|
||||
totalPages: formatCompactNumber(totalPages.value),
|
||||
},
|
||||
Number(totalPages.value)
|
||||
);
|
||||
return t('PAGINATION_FOOTER.CURRENT_PAGE_INFO', {
|
||||
currentPage: '',
|
||||
totalPages: totalPages.value,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -99,11 +91,9 @@ const pageInfo = computed(() => {
|
||||
/>
|
||||
<div class="inline-flex items-center gap-2 text-sm text-n-slate-11">
|
||||
<span class="px-3 tabular-nums py-0.5 bg-n-alpha-black2 rounded-md">
|
||||
{{ formatFullNumber(currentPage) }}
|
||||
</span>
|
||||
<span class="truncate">
|
||||
{{ pageInfo }}
|
||||
{{ currentPage }}
|
||||
</span>
|
||||
<span class="truncate">{{ pageInfo }}</span>
|
||||
</div>
|
||||
<Button
|
||||
icon="i-lucide-chevron-right"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"COMPANIES_LAYOUT": {
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
|
||||
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} companies"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} item | Showing {startItem} - {endItem} of {totalItems} items",
|
||||
"CURRENT_PAGE_INFO": "{currentPage} of {totalPages} page | {currentPage} of {totalPages} pages"
|
||||
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} items",
|
||||
"CURRENT_PAGE_INFO": "{currentPage} of {totalPages} pages"
|
||||
},
|
||||
"COMBOBOX": {
|
||||
"PLACEHOLDER": "Select an option...",
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
}
|
||||
},
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} contact | Showing {startItem} - {endItem} of {totalItems} contacts"
|
||||
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} contacts"
|
||||
},
|
||||
"FILTER": {
|
||||
"NAME": "Name",
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useNumberFormatter } from '../useNumberFormatter';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ref } from 'vue';
|
||||
|
||||
vi.mock('vue-i18n');
|
||||
|
||||
describe('useNumberFormatter', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useI18n).mockReturnValue({
|
||||
locale: ref('en-US'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatCompactNumber', () => {
|
||||
it('should return exact numbers for values under 1,000', () => {
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(0)).toBe('0');
|
||||
expect(formatCompactNumber(1)).toBe('1');
|
||||
expect(formatCompactNumber(42)).toBe('42');
|
||||
expect(formatCompactNumber(999)).toBe('999');
|
||||
});
|
||||
|
||||
it('should return "Xk" for exact thousands and "Xk+" for values with remainder', () => {
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(1000)).toBe('1k');
|
||||
expect(formatCompactNumber(1020)).toBe('1k+');
|
||||
expect(formatCompactNumber(1500)).toBe('1k+');
|
||||
expect(formatCompactNumber(1999)).toBe('1k+');
|
||||
expect(formatCompactNumber(2000)).toBe('2k');
|
||||
expect(formatCompactNumber(15000)).toBe('15k');
|
||||
expect(formatCompactNumber(15500)).toBe('15k+');
|
||||
expect(formatCompactNumber(999999)).toBe('999k+');
|
||||
});
|
||||
|
||||
it('should return millions/billion/trillion format for values 1,000,000 and above', () => {
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(1000000)).toBe('1M');
|
||||
expect(formatCompactNumber(1000001)).toBe('1.0M');
|
||||
expect(formatCompactNumber(1200000)).toBe('1.2M');
|
||||
expect(formatCompactNumber(1234000)).toBe('1.2M');
|
||||
expect(formatCompactNumber(2500000)).toBe('2.5M');
|
||||
expect(formatCompactNumber(10000000)).toBe('10M');
|
||||
expect(formatCompactNumber(1000000000)).toBe('1B');
|
||||
expect(formatCompactNumber(1100000000)).toBe('1.1B');
|
||||
expect(formatCompactNumber(10000000000)).toBe('10B');
|
||||
expect(formatCompactNumber(11000000000)).toBe('11B');
|
||||
expect(formatCompactNumber(1000000000000)).toBe('1T');
|
||||
expect(formatCompactNumber(1100000000000)).toBe('1.1T');
|
||||
expect(formatCompactNumber(10000000000000)).toBe('10T');
|
||||
expect(formatCompactNumber(11000000000000)).toBe('11T');
|
||||
});
|
||||
|
||||
it('should handle edge cases gracefully', () => {
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(null)).toBe('0');
|
||||
expect(formatCompactNumber(undefined)).toBe('0');
|
||||
expect(formatCompactNumber(NaN)).toBe('0');
|
||||
expect(formatCompactNumber('string')).toBe('0');
|
||||
});
|
||||
|
||||
it('should handle negative numbers', () => {
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(-500)).toBe('-500');
|
||||
expect(formatCompactNumber(-1000)).toBe('-1k');
|
||||
expect(formatCompactNumber(-1500)).toBe('-1k+');
|
||||
expect(formatCompactNumber(-2000)).toBe('-2k');
|
||||
expect(formatCompactNumber(-1200000)).toBe('-1.2M');
|
||||
});
|
||||
|
||||
it('should format with en-US locale', () => {
|
||||
const mockLocale = ref('en-US');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(1500)).toBe('1k+');
|
||||
});
|
||||
|
||||
it('should format with de-DE locale', () => {
|
||||
const mockLocale = ref('de-DE');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(2000)).toBe('2k');
|
||||
});
|
||||
|
||||
it('should format with fr-FR locale (compact notation)', () => {
|
||||
const mockLocale = ref('fr-FR');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
const result = formatCompactNumber(1000000);
|
||||
expect(result).toMatch(/1\s*M/); // French uses space before M
|
||||
});
|
||||
|
||||
it('should format with ja-JP locale', () => {
|
||||
const mockLocale = ref('ja-JP');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(999)).toBe('999');
|
||||
});
|
||||
|
||||
it('should format with ar-SA locale (Arabic numerals)', () => {
|
||||
const mockLocale = ref('ar-SA');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
const result = formatCompactNumber(5000);
|
||||
expect(result).toContain('k');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should format with es-ES locale', () => {
|
||||
const mockLocale = ref('es-ES');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(7500)).toBe('7k+');
|
||||
});
|
||||
|
||||
it('should format with hi-IN locale', () => {
|
||||
const mockLocale = ref('hi-IN');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(100000)).toBe('100k');
|
||||
});
|
||||
|
||||
it('should format with ru-RU locale', () => {
|
||||
const mockLocale = ref('ru-RU');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(3000)).toBe('3k');
|
||||
});
|
||||
|
||||
it('should format with ko-KR locale (uses 만 for 10,000)', () => {
|
||||
const mockLocale = ref('ko-KR');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
const result = formatCompactNumber(2500000);
|
||||
expect(result).toContain('만'); // Korean uses 만 (10,000) as a unit, so 2,500,000 should contain 만
|
||||
});
|
||||
|
||||
it('should format with pt-BR locale', () => {
|
||||
const mockLocale = ref('pt-BR');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatCompactNumber } = useNumberFormatter();
|
||||
expect(formatCompactNumber(8888)).toBe('8k+');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatFullNumber', () => {
|
||||
it('should format numbers with locale-specific formatting', () => {
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(1000)).toBe('1,000');
|
||||
expect(formatFullNumber(1234567)).toBe('1,234,567');
|
||||
expect(formatFullNumber(1234567890)).toBe('1,234,567,890');
|
||||
expect(formatFullNumber(1234567890123)).toBe('1,234,567,890,123');
|
||||
});
|
||||
|
||||
it('should handle edge cases gracefully', () => {
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(null)).toBe('0');
|
||||
expect(formatFullNumber(undefined)).toBe('0');
|
||||
expect(formatFullNumber(NaN)).toBe('0');
|
||||
expect(formatFullNumber('string')).toBe('0');
|
||||
});
|
||||
|
||||
it('should format with en-US locale (comma separator)', () => {
|
||||
const mockLocale = ref('en-US');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(1234567)).toBe('1,234,567');
|
||||
});
|
||||
|
||||
it('should format with de-DE locale (period separator)', () => {
|
||||
const mockLocale = ref('de-DE');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(9876543)).toBe('9.876.543');
|
||||
});
|
||||
|
||||
it('should format with es-ES locale (period separator)', () => {
|
||||
const mockLocale = ref('es-ES');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(5555555)).toBe('5.555.555');
|
||||
});
|
||||
|
||||
it('should format with zh-CN locale', () => {
|
||||
const mockLocale = ref('zh-CN');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(1000000)).toBe('1,000,000');
|
||||
});
|
||||
|
||||
it('should format with ar-EG locale (Arabic numerals, RTL)', () => {
|
||||
const mockLocale = ref('ar-EG');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
const result = formatFullNumber(7654321);
|
||||
// Arabic locale uses Eastern Arabic numerals (٠-٩)
|
||||
// Just verify it's formatted (length should be reasonable)
|
||||
expect(result.length).toBeGreaterThan(6);
|
||||
});
|
||||
|
||||
it('should format with fr-FR locale (narrow no-break space)', () => {
|
||||
const mockLocale = ref('fr-FR');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
const result = formatFullNumber(3333333);
|
||||
expect(result).toContain('3');
|
||||
expect(result).toContain('333');
|
||||
});
|
||||
|
||||
it('should format with hi-IN locale (Indian numbering system)', () => {
|
||||
const mockLocale = ref('hi-IN');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(9999999)).toBe('99,99,999');
|
||||
});
|
||||
|
||||
it('should format with th-TH locale', () => {
|
||||
const mockLocale = ref('th-TH');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(4444444)).toBe('4,444,444');
|
||||
});
|
||||
|
||||
it('should format with tr-TR locale', () => {
|
||||
const mockLocale = ref('tr-TR');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
expect(formatFullNumber(6666666)).toBe('6.666.666');
|
||||
});
|
||||
|
||||
it('should format with pt-PT locale (space separator)', () => {
|
||||
const mockLocale = ref('pt-PT');
|
||||
vi.mocked(useI18n).mockReturnValue({ locale: mockLocale });
|
||||
const { formatFullNumber } = useNumberFormatter();
|
||||
const result = formatFullNumber(2222222);
|
||||
// Portuguese (Portugal) uses narrow no-break space as separator
|
||||
expect(result).toMatch(/2[\s\u202f]222[\s\u202f]222/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
/**
|
||||
* Composable for number formatting with i18n locale support
|
||||
* Provides methods to format numbers in compact and full display formats
|
||||
*/
|
||||
export function useNumberFormatter() {
|
||||
const { locale } = useI18n();
|
||||
|
||||
/**
|
||||
* Formats numbers for display with clean, minimal formatting
|
||||
* - Up to 1,000: show exact number (e.g., 999)
|
||||
* - 1,000 to 999,999: show as "Xk" for exact thousands or "Xk+" for remainder (e.g., 1000 → "1k", 1500 → "1k+")
|
||||
* - 1,000,000+: show in millions with 1 decimal place (e.g., 1,234,000 → "1.2M")
|
||||
*
|
||||
* Uses browser-native Intl.NumberFormat with proper i18n locale support
|
||||
*
|
||||
* @param {number} num - The number to format
|
||||
* @returns {string} Formatted number string
|
||||
*/
|
||||
const formatCompactNumber = num => {
|
||||
if (typeof num !== 'number' || Number.isNaN(num)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// For numbers between -1000 and 1000 (exclusive), show exact number with locale formatting
|
||||
if (Math.abs(num) < 1000) {
|
||||
return new Intl.NumberFormat(locale.value).format(num);
|
||||
}
|
||||
|
||||
// For numbers with absolute value above 1,000,000, show in millions with 1 decimal place
|
||||
if (Math.abs(num) >= 1000000) {
|
||||
const millions = num / 1000000;
|
||||
return new Intl.NumberFormat(locale.value, {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
maximumFractionDigits: 1,
|
||||
minimumFractionDigits: millions % 1 === 0 ? 0 : 1,
|
||||
}).format(num);
|
||||
}
|
||||
|
||||
// For numbers with absolute value between 1,000 and 1,000,000, show as "Xk" or "Xk+" using floor value
|
||||
// For negative numbers, we want to floor towards zero (truncate), not towards negative infinity
|
||||
const thousands = num >= 0 ? Math.floor(num / 1000) : Math.ceil(num / 1000);
|
||||
const remainder = Math.abs(num) % 1000;
|
||||
const suffix = remainder === 0 ? 'k' : 'k+';
|
||||
return `${new Intl.NumberFormat(locale.value).format(thousands)}${suffix}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a number for full display with locale-specific formatting
|
||||
* @param {number} num - The number to format
|
||||
* @returns {string} Formatted number string with full precision and locale formatting (e.g., 1,234,567)
|
||||
*/
|
||||
const formatFullNumber = num => {
|
||||
if (typeof num !== 'number' || Number.isNaN(num)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat(locale.value).format(num);
|
||||
};
|
||||
|
||||
return {
|
||||
formatCompactNumber,
|
||||
formatFullNumber,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user