Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
db43626
feat: add LocaleProvider with URL-synced locale state
zigzagdev Apr 29, 2026
f688549
Merge pull request #300 from zigzagdev/feat/locale-provider
zigzagdev Apr 29, 2026
6afc555
Merge pull request #300 from zigzagdev/feat/locale-provider
zigzagdev Apr 29, 2026
4426a88
refactor: inline LocaleContextType in createContext call
zigzagdev Apr 29, 2026
66a0eff
refactor: consume locale via context in heritage detail
zigzagdev Apr 29, 2026
7ee99a0
fix: drop residual locale prop on HeritageOverViewSection call
zigzagdev Apr 29, 2026
486232b
Merge pull request #301 from zigzagdev/refactor/use-locale-context-in…
zigzagdev Apr 30, 2026
36de3ee
feat: add useText hook for locale-aware UI strings
zigzagdev Apr 30, 2026
861c567
refactor: localize HeritageCard via useText and useLocale
zigzagdev Apr 30, 2026
68a5481
refactor: localize heritage detail components via useText
zigzagdev Apr 30, 2026
4324a83
fix: const naming changed
zigzagdev Apr 30, 2026
8c8bc08
Merge pull request #302 from zigzagdev/chore/top-component-context-wr…
zigzagdev Apr 30, 2026
8244246
refactor: thread locale through heritage VM mappers
zigzagdev Apr 30, 2026
6d5ae1c
refactor: drop locale "ja" branches from heritage view components
zigzagdev Apr 30, 2026
f73fc00
feat: add ui-text keys for heritage sidebar labels
zigzagdev Apr 30, 2026
039f066
refactor: localize HeritageSidebar via useText
zigzagdev Apr 30, 2026
a846edc
chore: align ja sidebar labels to common reference wording
zigzagdev Apr 30, 2026
dd4fc94
refactor: rename useText() result to `text` in HeritageDetailLayout
zigzagdev May 1, 2026
eb51c4e
refactor: rename useText() result to `text` in HeritageSidebar
zigzagdev May 1, 2026
fe961df
refactor: rename useText() result to `text` in HeritageOverviewSection
zigzagdev May 1, 2026
97c0aa4
refactor: rename useText() result to `text` in HeritageHero
zigzagdev May 1, 2026
e01dfed
fix: restore `next` references in HeritageDetailLayout submit handler
zigzagdev May 1, 2026
ce038d9
feat: add categoryLabels and regionLabels to ui dictionaries
zigzagdev May 2, 2026
4f9c6ff
refactor: localize Category and Region values in HeritageSidebar
zigzagdev May 2, 2026
ce82607
refactor: localize Category and Region values in HeritageDetailLayout
zigzagdev May 2, 2026
f354884
feat: add search-form ui-text keys (all, keyword, year range, search)
zigzagdev May 2, 2026
e894216
refactor: localize HeritageSearchForm via useText
zigzagdev May 2, 2026
f445a52
refactor: drop export of TopPageProps and rename to file-local Props
zigzagdev May 2, 2026
d809891
refactor: drop export of Props in HeritageSubHeader
zigzagdev May 2, 2026
a0956b1
refactor: drop export of MetadataItem in HeritageMetadataList
zigzagdev May 2, 2026
197b09a
refactor: drop export of SearchResultsPageProps and rename to Props
zigzagdev May 2, 2026
788a492
refactor: decompose TopPage into TitleBar / List / Pagination subcomp…
zigzagdev May 2, 2026
030d067
refactor: extract LocaleToggle and reuse it in TitleBar / Detail
zigzagdev May 2, 2026
3eefbaa
feat: localize TopPage app title and tagline
zigzagdev May 2, 2026
f7abec0
chore: keep "World Heritage" untranslated in ja appTitle
zigzagdev May 2, 2026
5f4643b
feat: add sort/reload ui-text keys for title bar
zigzagdev May 2, 2026
fc728f7
refactor: localize sort options and Reload button in TopPageTitleBar
zigzagdev May 2, 2026
4e696f2
Merge pull request #303 from zigzagdev/chore/refactor-language-ja
zigzagdev May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const baseDto = (
});

describe("toWorldHeritageDetailVm", () => {
it("maps base fields and attaches sorted image VMs", () => {
it("en: maps base fields and attaches sorted image VMs", () => {
const dto = baseDto({
images: [
img({
Expand All @@ -68,13 +68,15 @@ describe("toWorldHeritageDetailVm", () => {
],
});

const vm: WorldHeritageDetailVm = toWorldHeritageDetailVm(dto);
const vm: WorldHeritageDetailVm = toWorldHeritageDetailVm(dto, "en");

expect(vm.id).toBe(dto.id);
expect(vm.title).toBe(dto.official_name);
expect(vm.title).toBe(dto.name);
expect(vm.country).toBe(dto.country);
expect(vm.countryNameJp).toBe(dto.country_name_jp);
expect(vm.region).toBe(dto.region);
expect(vm.displayDescription).toBe(dto.short_description);
expect(vm.displaySubName).toBeNull();

// detail
expect(vm.primaryStatePartyCode).toBe("JPN");
Expand All @@ -97,12 +99,23 @@ describe("toWorldHeritageDetailVm", () => {
expect(vm.images[1].alt).toBe("custom alt 2");
});

it("ja: title falls back to heritage_name_jp and displaySubName carries the English name", () => {
const dto = baseDto({ images: [] });

const vm = toWorldHeritageDetailVm(dto, "ja");

expect(vm.title).toBe(dto.heritage_name_jp);
expect(vm.displaySubName).toBe(dto.name);
expect(vm.displayDescription).toBe(dto.short_description_jp);
expect(vm.country).toBe(dto.country_name_jp);
});

it("is stable when images is empty array", () => {
const dto = baseDto({ images: [] });

const vm = toWorldHeritageDetailVm(dto);
const vm = toWorldHeritageDetailVm(dto, "en");

expect(vm.images).toEqual([]);
expect(vm.title).toBe(dto.official_name);
expect(vm.title).toBe(dto.name);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const base: ApiWorldHeritageDto = {
};

describe("toWorldHeritageVm", () => {
it("maps core fields + derived values correctly (DTO shape: thumbnail string)", () => {
const vm = toWorldHeritageVm(base);
it("ja: maps core fields + derived values using Japanese labels", () => {
const vm = toWorldHeritageVm(base, "ja");

expect(vm).toMatchObject({
id: 663,
Expand All @@ -54,6 +54,7 @@ describe("toWorldHeritageVm", () => {
criteriaText: "ix, x",
title: "白神山地",
subtitle: "日本 · Asia",
displayDescription: "ダミー",
areaText: "442 ha",
bufferText: "320 ha",
statePartyCodes: ["日本"],
Expand All @@ -63,48 +64,68 @@ describe("toWorldHeritageVm", () => {
});

expect(vm.criteria).toStrictEqual(["ix", "x"]);
// 英名 (Shirakami-Sanchi) は日本語タイトル (白神山地) と異なるので併記対象
expect(vm.displaySubName).toBe("Shirakami-Sanchi");
});

it("if official_name is empty, uses name as title", () => {
const vm = toWorldHeritageVm({ ...base, official_name: "" });
it("en: title/country/description fall back to English fields", () => {
const vm = toWorldHeritageVm(base, "en");

expect(vm.title).toBe("Shirakami-Sanchi");
expect(vm.country).toBe("Japan");
expect(vm.subtitle).toBe("Japan · Asia");
expect(vm.displayDescription).toBe("desc");
expect(vm.displaySubName).toBeNull();
});

it("ja: if official_name is empty, falls back to heritage_name_jp for title", () => {
const vm = toWorldHeritageVm({ ...base, official_name: "" }, "ja");
expect(vm.title).toBe("白神山地");
});

it("ja: if Japanese description is missing, falls back to English short_description", () => {
const vm = toWorldHeritageVm({ ...base, short_description_jp: null }, "ja");
expect(vm.displayDescription).toBe("desc");
});

it("when area/buffer are null, text becomes —", () => {
const vm = toWorldHeritageVm({
...base,
area_hectares: null,
buffer_zone_hectares: null,
});
const vm = toWorldHeritageVm(
{
...base,
area_hectares: null,
buffer_zone_hectares: null,
},
"ja",
);

expect(vm.areaText).toBe("—");
expect(vm.bufferText).toBe("—");
});

it("when thumbnail is null, thumbnailUrl is null", () => {
const vm = toWorldHeritageVm({ ...base, thumbnail: null });
const vm = toWorldHeritageVm({ ...base, thumbnail: null }, "ja");
expect(vm.thumbnailUrl).toBeNull();
});

it("input dto is immutable (criteria not mutated)", () => {
const dto: ApiWorldHeritageDto = { ...base, criteria: ["x", "ix", "ix"] };
const snapshot = [...dto.criteria];

const vm = toWorldHeritageVm(dto);
const vm = toWorldHeritageVm(dto, "ja");

expect(dto.criteria).toStrictEqual(snapshot);
expect(vm.criteria).toStrictEqual(["ix", "x"]);
});

it("unesco_site_url can be null (mapper should not crash)", () => {
const vm = toWorldHeritageVm({ ...base, unesco_site_url: null });
const vm = toWorldHeritageVm({ ...base, unesco_site_url: null }, "ja");
expect(vm.unescoSiteUrl).toBeNull();
});
});

describe("toWorldHeritageListVm", () => {
it("maps dto array into view model array", () => {
const vms = toWorldHeritageListVm([base, { ...base, id: 661 }]);
const vms = toWorldHeritageListVm([base, { ...base, id: 661 }], "ja");
expect(vms).toHaveLength(2);
expect(vms[0].id).toBe(663);
expect(vms[1].id).toBe(661);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import type {
WorldHeritageImageVm,
WorldHeritageVm,
} from "../../../../domain/types.ts";
import type { Locale } from "../../../../domain/criteria.ts";
import { toWorldHeritageVm } from "./to-world-heritage-vm.ts";

export function toWorldHeritageDetailVm(dto: ApiWorldHeritageDetailDto): WorldHeritageDetailVm {
export function toWorldHeritageDetailVm(
dto: ApiWorldHeritageDetailDto,
locale: Locale,
): WorldHeritageDetailVm {
const listDto = {
id: dto.id,
official_name: dto.official_name,
Expand All @@ -25,16 +29,15 @@ export function toWorldHeritageDetailVm(dto: ApiWorldHeritageDetailDto): WorldHe
area_hectares: dto.area_hectares,
buffer_zone_hectares: dto.buffer_zone_hectares,
short_description: dto.short_description,
short_description_jp: dto.short_description_jp,
unesco_site_url: dto.unesco_site_url,
state_party: dto.state_party,
state_party_codes: dto.state_party_codes,
state_parties_meta: dto.state_parties_meta,
thumbnail: dto.thumbnail_url,
} satisfies import("../../../../domain/types.ts").ApiWorldHeritageDto;

// Ensure the reshaped object satisfies ApiWorldHeritageDto at compile time

const base: WorldHeritageVm = toWorldHeritageVm(listDto);
const base: WorldHeritageVm = toWorldHeritageVm(listDto, locale);

const images: WorldHeritageImageVm[] = dto.images
.slice()
Expand Down
50 changes: 37 additions & 13 deletions client/src/app/features/heritages/mappers/to-world-heritage-vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type WorldHeritageVm,
type CriteriaCode,
} from "../../../../domain/types.ts";
import type { Locale } from "../../../../domain/criteria.ts";
import { statePartyLabels } from "@features/constants/state-party-labels.ts";
import { CRITERIA } from "../../../../domain/types.ts";

Expand All @@ -19,14 +20,33 @@ const normalizeCriteria = (values: readonly (string | CriteriaCode)[]): Criteria
const fmtHa = (value: number | null): string =>
value == null ? "—" : `${Number(value).toLocaleString("en-CA")} ha`;

const titleOf = (data: ApiWorldHeritageDto): string =>
data.heritage_name_jp || data.official_name || data.name;

const countryLabelOf = (data: ApiWorldHeritageDto): string | null =>
data.country_name_jp || data.country || null;
const titleOf = (data: ApiWorldHeritageDto, locale: Locale): string =>
locale === "ja"
? data.heritage_name_jp || data.official_name || data.name
: data.name || data.official_name || data.heritage_name_jp;

const countryLabelOf = (data: ApiWorldHeritageDto, locale: Locale): string | null =>
locale === "ja"
? data.country_name_jp || data.country || null
: data.country || data.country_name_jp || null;

const subtitleOf = (data: ApiWorldHeritageDto, locale: Locale): string =>
[countryLabelOf(data, locale), data.region].filter(Boolean).join(" · ");

// ja の時だけ、日本語名の脇に併記する英名を返す(一致や不在なら null)
const subNameOf = (data: ApiWorldHeritageDto, locale: Locale): string | null => {
if (locale !== "ja") return null;
if (!data.heritage_name_jp || !data.name) return null;
if (data.heritage_name_jp === data.name) return null;
return data.name;
};

const subtitleOf = (data: ApiWorldHeritageDto): string =>
[countryLabelOf(data), data.region].filter(Boolean).join(" · ");
const descriptionOf = (data: ApiWorldHeritageDto, locale: Locale): string => {
if (locale === "ja") {
return data.short_description_jp || data.short_description || "";
}
return data.short_description || "";
};

const toStatePartyLabelsJp = (codes: readonly string[]): string[] =>
codes.map((code) => statePartyLabels[code]).filter((label): label is string => Boolean(label));
Expand All @@ -49,7 +69,7 @@ const normalizeStatePartiesMeta = (
);
};

export function toWorldHeritageVm(data: ApiWorldHeritageDto): WorldHeritageVm {
export function toWorldHeritageVm(data: ApiWorldHeritageDto, locale: Locale): WorldHeritageVm {
const criteriaCodes = normalizeCriteria(data.criteria);
const statePartyCodesRaw = data.state_party_codes ?? [];
const statePartyLabelsJp = toStatePartyLabelsJp(statePartyCodesRaw);
Expand All @@ -66,7 +86,7 @@ export function toWorldHeritageVm(data: ApiWorldHeritageDto): WorldHeritageVm {
name: data.name,
heritageNameJp: data.heritage_name_jp,

country: data.country_name_jp,
country: countryLabelOf(data, locale) ?? "",
countryNameJp: data.country_name_jp,
region: data.region,
stateParty,
Expand All @@ -86,8 +106,10 @@ export function toWorldHeritageVm(data: ApiWorldHeritageDto): WorldHeritageVm {
statePartiesMeta: normalizeStatePartiesMeta(data.state_parties_meta),
primaryStatePartyCode: null,

title: titleOf(data),
subtitle: subtitleOf(data),
title: titleOf(data, locale),
subtitle: subtitleOf(data, locale),
displaySubName: subNameOf(data, locale),
displayDescription: descriptionOf(data, locale),
areaText: fmtHa(data.area_hectares),
bufferText: fmtHa(data.buffer_zone_hectares),
criteriaText: criteriaCodes.join(", "),
Expand All @@ -97,5 +119,7 @@ export function toWorldHeritageVm(data: ApiWorldHeritageDto): WorldHeritageVm {
};
}

export const toWorldHeritageListVm = (list: ApiWorldHeritageDto[]): WorldHeritageVm[] =>
list.map(toWorldHeritageVm);
export const toWorldHeritageListVm = (
list: ApiWorldHeritageDto[],
locale: Locale,
): WorldHeritageVm[] => list.map((data) => toWorldHeritageVm(data, locale));
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Pagination } from "@features/top/components/Pagination.tsx";
import { BreadcrumbList } from "@shared/components/BreadcrumbList.tsx";
import { SearchResultMapComponent } from "@features/search/components/SearchResultMapComponent.tsx";

export type SearchResultsPageProps = {
type Props = {
header?: ReactNode;
items: WorldHeritageVm[];
pagination: SearchResultsPagination | null;
Expand All @@ -30,7 +30,7 @@ export default function SearchResultsPage({
errorMessage,
onPageChange,
onBackToAllSites,
}: SearchResultsPageProps) {
}: Props) {
return (
<main className="mx-auto max-w-7xl px-4 py-12">
<div className="sticky top-0 z-20 -mx-4 border-b border-zinc-200 bg-white/95 px-4 py-3 backdrop-blur">
Expand Down
Loading
Loading