diff --git a/apps/web/e2e/category-spec-template.spec.ts b/apps/web/e2e/category-spec-template.spec.ts index d92b1123..06c4b9e3 100644 --- a/apps/web/e2e/category-spec-template.spec.ts +++ b/apps/web/e2e/category-spec-template.spec.ts @@ -10,12 +10,13 @@ test('earbud page shows audio specs and no phone fields', async ({ page }) => { await page.goto(BUD); const specs = page.locator('#specs-section'); const headings = specs.locator('h3'); - await expect(headings.first()).toHaveText(/^1\. Âm thanh/); + // One language and no number (DOS/Rate#1529). + await expect(headings.first()).toHaveText(/^Âm thanh/); // Groups with no value are hidden (DOS/Rate#1186) and no sale status is // invented (DOS/Rate#1193): the seeded bud has audio, battery and // Bluetooth, nothing else. await expect(headings).toHaveCount(3); - await expect(headings.nth(1)).toHaveText(/^2\. Pin/); + await expect(headings.nth(1)).toHaveText(/^Pin/); await expect(specs).toContainText('E2E 10.5mm driver'); await expect(specs).toContainText('E2E ANC'); for (const text of [ @@ -62,11 +63,13 @@ test('a watch page never shows 5G', async ({ page }) => { await expect(meta).toContainText('Apple'); await expect(meta).not.toContainText('5G'); const specs = page.locator('#specs-section'); - await expect(specs).toContainText('Màn hình (Display)'); + await expect(specs).toContainText('Màn hình'); + // One language per page (DOS/Rate#1529). + await expect(specs).not.toContainText('(Display)'); // The seeded watch has no network value left once 5G is dropped, so the // cellular group is hidden (DOS/Rate#1186); the 4G-only bands label is // covered by product-spec-rows.test.ts. - await expect(specs).not.toContainText('Mạng Di động'); + await expect(specs).not.toContainText('Mạng di động'); await expect(specs).not.toContainText('5G'); await expect(specs).not.toContainText('Camera'); }); diff --git a/apps/web/locales/cs.json b/apps/web/locales/cs.json index 42318287..932c8f20 100644 --- a/apps/web/locales/cs.json +++ b/apps/web/locales/cs.json @@ -201,7 +201,7 @@ "catEarbudsUnit": "modelů sluchátek", "boolYes": "Ano", "boolNo": "Ne", - "group0Title": "0. Hodnocení a důvěryhodnost", + "group0Title": "Hodnocení a důvěryhodnost", "rowRatingWeighted": "Skutečné hodnocení kupujících (bez falešných recenzí)", "valAuthenticReviews": "★ {rating} / 5,0 ({count, plural, one {# ověřená recenze} few {# ověřené recenze} other {# ověřených recenzí}})", "valNoReviews": "Zatím bez hodnocení", @@ -229,7 +229,7 @@ "rowCamSecond": "Druhý fotoaparát (Ultraširokoúhlý)", "rowCamThirdFourth": "Třetí a čtvrtý fotoaparát (Tele / Periskop)", "rowCamVideo": "Záznam videa a blesk", - "rowCamFront": "Přední kamera (Selfie)", + "rowCamFront": "Přední kamera", "rowBatCapacity": "Kapacita baterie", "rowBatFastCharge": "Výkon rychlého nabíjení", "rowBatDetails": "Technologie baterie, bezdrátové a reverzní nabíjení", @@ -946,7 +946,7 @@ "listingsSectionSubtitle": "Porovnání cen u prodejce {name} s nejnižší tržní cenou a historickým mediánem", "noListingsYet": "K tomuto obchodu zatím nejsou přiřazeny žádné produkty.", "colListedProduct": "Nabízený produkt", - "colMasterProduct": "Standardizovaný model (Master Product)", + "colMasterProduct": "Standardizovaný model", "colRecordedPrice": "Zaznamenaná cena", "colPromoAudit": "Sleva (podle historie cen)", "colAuditAndSource": "Kontrola a zdroj", @@ -1796,11 +1796,11 @@ "jreviewsRatingHeader": "Hodnocení (JReviews)", "ratingAll": "Vše", "categoryHeader": "Kategorie zařízení", - "brandHeader": "Výrobce (Brand)", + "brandHeader": "Výrobce", "priceRangeHeader": "Cenové rozpětí (Miliony VND)", - "ramHeader": "Kapacita RAM (Hardware)", + "ramHeader": "Kapacita RAM", "storageHeader": "Interní úložiště (ROM)", - "featuresHeader": "Klíčové funkce (10 skupin parametrů)", + "featuresHeader": "Klíčové funkce", "filteringBtn": "Filtrování dat...", "applyFiltersBtn": "Použít filtry", "clearAllFiltersBtn": "Vymazat všechny filtry", diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index 6bc16f26..2f64d7bd 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -201,7 +201,7 @@ "catEarbudsUnit": "earbud models", "boolYes": "Yes", "boolNo": "No", - "group0Title": "0. Review scores and trust", + "group0Title": "Review scores and trust", "rowRatingWeighted": "Verified buyer score (fake reviews filtered)", "valAuthenticReviews": "★ {rating} / 5.0 ({count, plural, one {# verified review} other {# verified reviews}})", "valNoReviews": "No reviews yet", @@ -946,7 +946,7 @@ "listingsSectionSubtitle": "Comparing prices recorded at {name} against the market lowest price and historical median", "noListingsYet": "No product listings linked to this merchant yet.", "colListedProduct": "Listed product", - "colMasterProduct": "Canonical model (master product)", + "colMasterProduct": "Canonical model", "colRecordedPrice": "Recorded price", "colPromoAudit": "Discount (by price history)", "colAuditAndSource": "Check and source", @@ -1796,9 +1796,9 @@ "jreviewsRatingHeader": "Rating (JReviews)", "ratingAll": "All", "categoryHeader": "Device category", - "brandHeader": "Manufacturer (Brand)", + "brandHeader": "Manufacturer", "priceRangeHeader": "Price range (million VND)", - "ramHeader": "RAM Capacity (Hardware)", + "ramHeader": "RAM Capacity", "storageHeader": "Internal storage (ROM)", "featuresHeader": "Key features", "filteringBtn": "Filtering data...", diff --git a/apps/web/locales/vi.json b/apps/web/locales/vi.json index 87498554..12be947d 100644 --- a/apps/web/locales/vi.json +++ b/apps/web/locales/vi.json @@ -201,7 +201,7 @@ "catEarbudsUnit": "mẫu tai nghe", "boolYes": "Có", "boolNo": "Không", - "group0Title": "0. Điểm đánh giá và độ tin cậy", + "group0Title": "Điểm đánh giá và độ tin cậy", "rowRatingWeighted": "Điểm người mua thật (đã lọc đánh giá ảo)", "valAuthenticReviews": "★ {rating} / 5.0 ({count} review thật)", "valNoReviews": "Chưa có đánh giá", @@ -219,7 +219,7 @@ "rowDisplayResolution": "Độ phân giải và mật độ điểm ảnh", "rowDisplayRefresh": "Tần số quét", "rowDisplayProtection": "Kính bảo vệ và tính năng hiển thị", - "rowHwChipset": "Vi xử lý (Chipset)", + "rowHwChipset": "Vi xử lý", "rowHwCpuGpu": "Nhân CPU và đồ họa GPU", "rowHwRam": "Dung lượng RAM", "rowHwStorage": "Bộ nhớ trong (ROM)", @@ -229,7 +229,7 @@ "rowCamSecond": "Camera thứ 2 (góc siêu rộng / phụ)", "rowCamThirdFourth": "Camera thứ 3 và thứ 4 (tele / tiềm vọng)", "rowCamVideo": "Quay video và đèn flash", - "rowCamFront": "Camera trước (selfie)", + "rowCamFront": "Camera trước", "rowBatCapacity": "Dung lượng pin", "rowBatFastCharge": "Công suất sạc nhanh", "rowBatDetails": "Công nghệ pin, sạc không dây và sạc ngược", @@ -946,7 +946,7 @@ "listingsSectionSubtitle": "Đối chiếu giá ghi nhận tại {name} với mức giá thấp nhất thị trường và trung vị lịch sử giá", "noListingsYet": "Chưa có sản phẩm nào được liên kết với gian hàng này.", "colListedProduct": "Sản phẩm niêm yết", - "colMasterProduct": "Dòng máy chuẩn hóa (master product)", + "colMasterProduct": "Dòng máy chuẩn hóa", "colRecordedPrice": "Giá ghi nhận", "colPromoAudit": "Khuyến mãi (theo lịch sử giá)", "colAuditAndSource": "Kiểm tra và nguồn", @@ -1423,7 +1423,7 @@ "pgPreset13Title": "Spec OpenAPI 3.1.0 (/openapi.json)", "pgPreset13Desc": "Trả về spec OpenAPI 3.1.0 dạng JSON cho các endpoint REST API v1 của Rate.vn.", "authPublic": "Công khai (CORS)", - "authOptIn": "Cần người dùng đồng ý (opt-in)", + "authOptIn": "Cần người dùng đồng ý", "authBearer": "Bearer token (Supabase Auth)", "cacheNoStoreQueue": "no-store (đưa vào hàng đợi rate.crawl_jobs)", "cacheNoStoreRpc": "no-store (RPC rate.submit_community_review)", @@ -1478,7 +1478,7 @@ "aboutQuickLinksTitle": "Liên kết nhanh trên hệ thống", "aboutLinkSmartphones": "Danh mục điện thoại thông minh", "aboutLinkLaptops": "Danh mục laptop & ultrabook", - "aboutLinkTablets": "Danh mục máy tính bảng (tablet)", + "aboutLinkTablets": "Danh mục máy tính bảng", "aboutLinkFaq": "Câu hỏi thường gặp về phương pháp tính điểm", "privacySeoTitle": "Chính sách bảo mật và dữ liệu | Rate.vn", "privacySeoDesc": "Rate.vn không cần tài khoản để tra cứu. Khay so sánh lưu trên trình duyệt, bình chọn gắn với DOS ID và thông tin riêng của gian hàng không công khai.", @@ -1796,11 +1796,11 @@ "jreviewsRatingHeader": "Điểm đánh giá (JReviews)", "ratingAll": "Tất cả", "categoryHeader": "Danh mục thiết bị", - "brandHeader": "Hãng sản xuất (Brand)", + "brandHeader": "Hãng sản xuất", "priceRangeHeader": "Khoảng giá (Triệu VNĐ)", - "ramHeader": "Dung lượng RAM (Hardware)", + "ramHeader": "Dung lượng RAM", "storageHeader": "Bộ nhớ trong (ROM)", - "featuresHeader": "Tính năng nổi bật (10 field groups)", + "featuresHeader": "Tính năng nổi bật", "filteringBtn": "Đang lọc dữ liệu...", "applyFiltersBtn": "Áp dụng bộ lọc", "clearAllFiltersBtn": "Xóa tất cả bộ lọc", diff --git a/apps/web/src/app/[locale]/smartphones/[slug]/page.tsx b/apps/web/src/app/[locale]/smartphones/[slug]/page.tsx index 8acda22d..d836a67a 100644 --- a/apps/web/src/app/[locale]/smartphones/[slug]/page.tsx +++ b/apps/web/src/app/[locale]/smartphones/[slug]/page.tsx @@ -1465,16 +1465,11 @@ export async function ProductDetailView({ openOnHash="#specs-section" collapsible={specGroups.length > 3} > - {specGroups.map(({ group, rows }, i) => ( + {specGroups.map(({ group, rows }) => (

- {specGroupHeading( - specUi, - group, - i + 1, - specTemplate.has5g - )} + {specGroupHeading(specUi, group, specTemplate.has5g)}

{rows.map(({ label, value, lang }) => ( diff --git a/apps/web/src/components/compare/CompareWorkspace.tsx b/apps/web/src/components/compare/CompareWorkspace.tsx index cb626d88..39f52b98 100644 --- a/apps/web/src/components/compare/CompareWorkspace.tsx +++ b/apps/web/src/components/compare/CompareWorkspace.tsx @@ -1052,10 +1052,18 @@ export default function CompareWorkspace({ ), })) .filter((group) => group.rows.length > 0); - // Spec groups are numbered from 1 after the market group. - const numberedSpecGroups = visibleSpecGroups.filter( - (group) => group.id !== 'market_trust' - ); + + // A spec group's heading in the page's language, for the table and the + // exports alike (DOS/Rate#1529); a watch's cellular heading has no "5G" + // (DOS/Rate#1166). + const groupHeading = (group: { id: string; title: string }) => + group.id in specDict.groups + ? specGroupHeading( + specDict, + group.id as SpecGroupKey, + getCategorySpecTemplate(activeCategory).has5g + ) + : group.title; const searchInputRef = useRef(null); const searchBoxRef = useRef(null); @@ -1479,7 +1487,7 @@ export default function CompareWorkspace({ const vals = products.map((p) => row.renderValue(p)); if (vals.every((v) => !v || v === '-')) continue; rows.push( - [group.title, row.label, ...vals] + [groupHeading(group), row.label, ...vals] .map((c) => escapeCsvCell(c)) .join(',') ); @@ -1518,7 +1526,7 @@ export default function CompareWorkspace({ }), matrix: specGroups.map((group) => ({ groupId: group.id, - groupTitle: group.title, + groupTitle: groupHeading(group), // Like the CSV, no row that every product leaves empty. rows: group.rows .filter((row) => @@ -2559,18 +2567,7 @@ export default function CompareWorkspace({ 1 + (products.length < MAX_COMPARE_ITEMS ? 1 : 0); - // Numbered in the category's order, and a watch's - // cellular heading without "5G" (DOS/Rate#1166). The - // market group comes first, so spec groups start at 1. - const groupTitle = - group.id in specDict.groups - ? specGroupHeading( - specDict, - group.id as SpecGroupKey, - numberedSpecGroups.indexOf(group) + 1, - getCategorySpecTemplate(activeCategory).has5g - ) - : group.title; + const groupTitle = groupHeading(group); return ( diff --git a/apps/web/src/lib/__tests__/product-detail-copy.test.ts b/apps/web/src/lib/__tests__/product-detail-copy.test.ts index b65fcc7c..e086e28c 100644 --- a/apps/web/src/lib/__tests__/product-detail-copy.test.ts +++ b/apps/web/src/lib/__tests__/product-detail-copy.test.ts @@ -190,7 +190,7 @@ function main() { 'Hexa-core (2x4.05 GHz performance + 4x2.42 GHz efficiency)', 'vi' ), - '6 nhân (Hexa-core) (2x4.05 GHz hiệu năng + 4x2.42 GHz tiết kiệm điện)' + '6 nhân (2x4.05 GHz hiệu năng + 4x2.42 GHz tiết kiệm điện)' ); assert.equal( localizeSpecValue( diff --git a/apps/web/src/lib/__tests__/spec-template.test.ts b/apps/web/src/lib/__tests__/spec-template.test.ts index 763772f3..c1660fc6 100644 --- a/apps/web/src/lib/__tests__/spec-template.test.ts +++ b/apps/web/src/lib/__tests__/spec-template.test.ts @@ -18,8 +18,8 @@ const readSource = (path: string) => const headings = (category: string, locale: string) => { const ui = getSpecUiDictionary(locale); const template = getCategorySpecTemplate(category); - return template.groups.map((group: SpecGroupKey, i: number) => - specGroupHeading(ui, group, i + 1, template.has5g) + return template.groups.map((group: SpecGroupKey) => + specGroupHeading(ui, group, template.has5g) ); }; @@ -216,8 +216,11 @@ async function testSurfaces() { // Compare headings come from the template too: no "5G" heading for a watch. assert.match( compare, - /specGroupHeading\(\s*specDict,\s*group\.id as SpecGroupKey,\s*numberedSpecGroups\.indexOf\(group\) \+ 1,\s*getCategorySpecTemplate\(activeCategory\)\.has5g\s*\)/ + /specGroupHeading\(\s*specDict,\s*group\.id as SpecGroupKey,\s*getCategorySpecTemplate\(activeCategory\)\.has5g\s*\)/ ); + // The CSV and JSON exports use the same heading as the table. + assert.match(compare, /\[groupHeading\(group\), row\.label/); + assert.match(compare, /groupTitle: groupHeading\(group\)/); const detail = readSource('../../app/[locale]/smartphones/[slug]/page.tsx'); assert.match(detail, /categorySlug=\{categorySlug\}/); assert.match(detail, /buildKeySpecLine\(product, categorySlug\)/); @@ -229,12 +232,25 @@ async function testSurfaces() { async function main() { console.warn('🧪 Category spec template suite (DOS/Rate#1142)'); - // Phones keep the ten numbered groups, worded as before. + // Phones keep the ten groups, each in the page's language only: no + // number and no English name in brackets (DOS/Rate#1529). const phone = headings('smartphones', 'vi'); assert.equal(phone.length, 10); - assert.equal(phone[0], '1. Màn hình (Display)'); - assert.equal(phone[5], '6. Mạng Di động & 5G (Cellular)'); - assert.equal(phone[9], '10. Pháp lý, Chứng nhận & SAR (Regulatory)'); + assert.equal(phone[0], 'Màn hình'); + assert.equal(phone[1], 'Phần cứng & Hiệu năng'); + assert.equal(phone[5], 'Mạng di động & 5G'); + assert.equal(phone[9], 'Pháp lý, Chứng nhận & SAR'); + assert.equal(headings('smartphones', 'en')[0], 'Display & panel'); + for (const locale of ['vi', 'en', 'cs']) { + for (const h of [ + ...headings('smartphones', locale), + getSpecUiDictionary(locale).cellularNo5gGroup, + ]) { + assert.doesNotMatch(h, /^\d/, `${locale}: ${h}`); + // Acronyms such as (MSRP) may stay; a Vietnamese label has none left. + if (locale === 'vi') assert.doesNotMatch(h, /\(/, h); + } + } assert.deepEqual(headings('tablets', 'en'), headings('smartphones', 'en')); assert.equal( specSectionBadge(getSpecUiDictionary('vi'), 10), @@ -247,25 +263,23 @@ async function main() { const ui = getSpecUiDictionary(locale); const buds = headings('earbuds', locale); assert.equal(buds.length, 7); - assert.ok(buds[0]!.startsWith('1. ')); - assert.ok(buds[0]!.endsWith(ui.groups.multimedia.replace(/^\d+\.\s*/, ''))); + assert.equal(buds[0], ui.groups.multimedia); for (const group of ['display', 'camera', 'cellular'] as const) { - const label = ui.groups[group].replace(/^\d+\.\s*/, ''); assert.ok( - !buds.some((h: string) => h.endsWith(label)), + !buds.includes(ui.groups[group]), `earbuds (${locale}) must not show ${group}` ); } assert.equal(specSectionBadge(ui, buds.length).startsWith('7 '), true); } assert.deepEqual(headings('earbuds', 'en'), [ - '1. Audio & multimedia', - '2. Battery & charging', - '3. Connectivity & sensors', - '4. Hardware & performance', - '5. Design & durability', - '6. Availability & MSRP', - '7. Regulatory, certifications & SAR', + 'Audio & multimedia', + 'Battery & charging', + 'Connectivity & sensors', + 'Hardware & performance', + 'Design & durability', + 'Availability & MSRP', + 'Regulatory, certifications & SAR', ]); // Smartwatches: no camera group and no "5G" anywhere in the headings. @@ -277,7 +291,7 @@ async function main() { watch.join(' | ') ); } - assert.equal(headings('smartwatches', 'vi')[4], '5. Mạng Di động (Cellular)'); + assert.equal(headings('smartwatches', 'vi')[4], 'Mạng di động'); // The card chips read only what the template leaves: an earbud has no // "1GB RAM · 64GB" and a cellular watch no "5G". diff --git a/apps/web/src/lib/spec-localizer.ts b/apps/web/src/lib/spec-localizer.ts index f146d3fc..95c705fd 100644 --- a/apps/web/src/lib/spec-localizer.ts +++ b/apps/web/src/lib/spec-localizer.ts @@ -94,18 +94,18 @@ const SPEC_UI_DICTS: Record = { no: 'Không', supported: 'Có hỗ trợ', has5gYes: 'Có (5G Sub-6 / mmWave)', - cellularNo5gGroup: '6. Mạng Di động (Cellular)', + cellularNo5gGroup: 'Mạng di động', groups: { - display: '1. Màn hình (Display)', - hardware: '2. Phần cứng & Hiệu năng (Hardware)', - camera: '3. Máy ảnh & Quang học (Camera - PhoneArena Split)', - battery: '4. Pin & Công nghệ Sạc (Battery)', - design: '5. Thiết kế & Độ bền (Design)', - cellular: '6. Mạng Di động & 5G (Cellular)', - connectivity: '7. Kết nối & Cảm biến (Connectivity)', - multimedia: '8. Âm thanh & Đa phương tiện (Multimedia)', - availability: '9. Phát hành & Giá niêm yết (Availability)', - regulatory: '10. Pháp lý, Chứng nhận & SAR (Regulatory)', + display: 'Màn hình', + hardware: 'Phần cứng & Hiệu năng', + camera: 'Máy ảnh & Quang học', + battery: 'Pin & Công nghệ sạc', + design: 'Thiết kế & Độ bền', + cellular: 'Mạng di động & 5G', + connectivity: 'Kết nối & Cảm biến', + multimedia: 'Âm thanh & Đa phương tiện', + availability: 'Phát hành & Giá niêm yết', + regulatory: 'Pháp lý, Chứng nhận & SAR', }, fields: { screenSize: 'Kích thước màn hình', @@ -115,7 +115,7 @@ const SPEC_UI_DICTS: Record = { ppi: 'Mật độ điểm ảnh', protection: 'Kính bảo vệ', displayFeatures: 'Tính năng hiển thị', - chipset: 'Vi xử lý (Chipset)', + chipset: 'Vi xử lý', cpu: 'Nhân CPU', gpu: 'Đồ họa (GPU)', ram: 'Bộ nhớ RAM', @@ -127,7 +127,7 @@ const SPEC_UI_DICTS: Record = { secondCamera: 'Camera phụ 2 (Góc siêu rộng)', thirdCamera: 'Camera phụ 3 (Tele / Zoom)', fourthCamera: 'Camera phụ 4 (Tiềm vọng / Cảm biến)', - frontCamera: 'Camera trước (Selfie)', + frontCamera: 'Camera trước', videoAndFlash: 'Quay video & Đèn Flash', batteryCapacity: 'Dung lượng pin', batteryType: 'Loại pin', @@ -174,18 +174,18 @@ const SPEC_UI_DICTS: Record = { no: 'No', supported: 'Supported', has5gYes: 'Yes (5G Sub-6 / mmWave)', - cellularNo5gGroup: '6. Cellular network', + cellularNo5gGroup: 'Cellular network', groups: { - display: '1. Display & panel', - hardware: '2. Hardware & performance', - camera: '3. Camera system (PhoneArena Split)', - battery: '4. Battery & charging', - design: '5. Design & durability', - cellular: '6. Cellular & 5G network', - connectivity: '7. Connectivity & sensors', - multimedia: '8. Audio & multimedia', - availability: '9. Availability & MSRP', - regulatory: '10. Regulatory, certifications & SAR', + display: 'Display & panel', + hardware: 'Hardware & performance', + camera: 'Camera system', + battery: 'Battery & charging', + design: 'Design & durability', + cellular: 'Cellular & 5G network', + connectivity: 'Connectivity & sensors', + multimedia: 'Audio & multimedia', + availability: 'Availability & MSRP', + regulatory: 'Regulatory, certifications & SAR', }, fields: { screenSize: 'Screen size', @@ -254,18 +254,18 @@ const SPEC_UI_DICTS: Record = { no: 'Ne', supported: 'Podporováno', has5gYes: 'Ano (5G Sub-6 / mmWave)', - cellularNo5gGroup: '6. Mobilní sítě', + cellularNo5gGroup: 'Mobilní sítě', groups: { - display: '1. Displej a obrazovka', - hardware: '2. Hardware a výkon', - camera: '3. Fotoaparát a optika (PhoneArena Split)', - battery: '4. Baterie a nabíjení', - design: '5. Konstrukce a odolnost', - cellular: '6. Mobilní sítě a 5G', - connectivity: '7. Konektivita a senzory', - multimedia: '8. Zvuk a multimédia', - availability: '9. Dostupnost a cena (MSRP)', - regulatory: '10. Certifikace a hodnota SAR', + display: 'Displej a obrazovka', + hardware: 'Hardware a výkon', + camera: 'Fotoaparát a optika', + battery: 'Baterie a nabíjení', + design: 'Konstrukce a odolnost', + cellular: 'Mobilní sítě a 5G', + connectivity: 'Konektivita a senzory', + multimedia: 'Zvuk a multimédia', + availability: 'Dostupnost a cena (MSRP)', + regulatory: 'Certifikace a hodnota SAR', }, fields: { screenSize: 'Úhlopříčka displeje', @@ -283,11 +283,11 @@ const SPEC_UI_DICTS: Record = { storageExpansion: 'Rozšíření paměti', os: 'Operační systém', rearSetup: 'Sestava zadního fotoaparátu', - mainCamera: 'Hlavní snímač (Wide)', - secondCamera: 'Druhý snímač (Ultrawide)', + mainCamera: 'Hlavní snímač (širokoúhlý)', + secondCamera: 'Druhý snímač (ultraširokoúhlý)', thirdCamera: 'Třetí snímač (Teleobjektiv)', fourthCamera: 'Čtvrtý snímač (Periskop / ToF)', - frontCamera: 'Přední kamera (Selfie)', + frontCamera: 'Přední kamera', videoAndFlash: 'Záznam videa a blesk', batteryCapacity: 'Kapacita baterie', batteryType: 'Typ baterie', @@ -333,18 +333,17 @@ export function getSpecUiDictionary(locale?: string): SpecUiDictionary { } /** - * A group heading numbered by its place in the category's template, so an - * earbud page that starts with audio reads "1. Audio" (DOS/Rate#1142). + * A group heading in the page's language, without a number or a second + * language (DOS/Rate#1529); a watch's cellular group has no "5G". */ export function specGroupHeading( ui: SpecUiDictionary, group: SpecGroupKey, - position: number, has5g = true ): string { - const label = - group === 'cellular' && !has5g ? ui.cellularNo5gGroup : ui.groups[group]; - return `${position}. ${label.replace(/^\d+\.\s*/, '')}`; + return group === 'cellular' && !has5g + ? ui.cellularNo5gGroup + : ui.groups[group]; } /** The "10 spec groups" badge with the category's own group count. */ @@ -445,9 +444,9 @@ const VI_SPEC_REPLACEMENTS: Array<[RegExp, string]> = [ [/\bperformance\b/gi, 'hiệu năng'], [/\befficiency\b/gi, 'tiết kiệm điện'], [/\(photo\/panorama\)/gi, '(ảnh/toàn cảnh)'], - [/\bOcta-core\b/gi, '8 nhân (Octa-core)'], - [/\bHexa-core\b/gi, '6 nhân (Hexa-core)'], - [/\bDeca-core\b/gi, '10 nhân (Deca-core)'], + [/\bOcta-core\b/gi, '8 nhân'], + [/\bHexa-core\b/gi, '6 nhân'], + [/\bDeca-core\b/gi, '10 nhân'], ]; // A letter only Vietnamese uses among the spec languages.