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.