Replace map grids with hierarchical clustering

This commit is contained in:
SimpleTest 2026-07-29 02:54:12 +03:00
parent 762b72100d
commit ee6a1f7e70
20 changed files with 1583 additions and 1022 deletions

View File

@ -1,8 +1,10 @@
import maplibregl from "maplibre-gl"
import {
clusterCountLabel,
clusterExpansionTarget,
clusterSizeClass,
mergeNearbyMapPoints
} from "./map_cluster_points.mjs"
pointFeatureCollection
} from "./map_cluster_features.mjs"
const trackingMinTimeMs = 5000
@ -152,22 +154,6 @@ const areaFeatureCollection = points => ({
.map(areaPolygon)
})
const clusterFeatureCollection = points => ({
type: "FeatureCollection",
features: points
.filter(point => point.type === "cluster")
.map(point => ({
type: "Feature",
id: point.id,
properties: {
id: point.id,
count: point.count,
urgent_count: point.urgent_count || 0
},
geometry: {type: "Point", coordinates: [point.longitude, point.latitude]}
}))
})
const mapViewport = map => {
const bounds = map.getBounds()
const container = map.getContainer()
@ -300,7 +286,10 @@ const createAidMap = element => {
resizeObserver: null,
markers: [],
markerElements: new Map(),
points: [],
selectedItemId: null,
clusterExpansionPending: false,
pendingMarkerRender: false,
initializedViewport: false,
locationBounds: null,
recenterControl: null,
@ -310,7 +299,9 @@ const createAidMap = element => {
areaSelectedId: `${element.id || "aid-map"}-area-selected`,
clusterSourceId: `${element.id || "aid-map"}-clusters`,
clusterCircleId: `${element.id || "aid-map"}-cluster-circle`,
clusterCountId: `${element.id || "aid-map"}-cluster-count`,
serverClusterCircleId: `${element.id || "aid-map"}-server-cluster-circle`,
pointCircleId: `${element.id || "aid-map"}-point-circle`,
pointSelectedId: `${element.id || "aid-map"}-point-selected`,
statusBadge: element.parentElement?.querySelector("[data-map-status-badge]") || null
}
@ -458,6 +449,8 @@ const createAidMap = element => {
}
state.highlightRequest = id => {
state.selectedItemId = id || null
state.markerElements.forEach((element, requestId) => {
element.classList.toggle("is-selected", requestId === id)
})
@ -465,6 +458,10 @@ const createAidMap = element => {
if (state.map?.getLayer(state.areaSelectedId)) {
state.map.setFilter(state.areaSelectedId, ["==", ["get", "id"], id || ""])
}
if (state.map?.getLayer(state.pointSelectedId)) {
state.map.setFilter(state.pointSelectedId, ["==", ["get", "id"], id || ""])
}
}
state.selectRequest = id => {
@ -486,7 +483,6 @@ const createAidMap = element => {
const current = state.element.getBoundingClientRect()
if (current.width >= 160 && current.height >= 160) {
state.map.resize()
if (state.element.dataset.discoveryMap === "true") state.renderMarkers()
}
})
}
@ -517,6 +513,181 @@ const createAidMap = element => {
return true
}
state.expandNativeCluster = feature => {
const source = state.map?.getSource(state.clusterSourceId)
const clusterId = Number(feature?.properties?.cluster_id)
if (!source || !Number.isFinite(clusterId)) return
Promise.resolve(source.getClusterExpansionZoom(clusterId)).then(expansionZoom => {
if (!state.active || !state.map) return
const currentZoom = state.map.getZoom()
const targetZoom = clusterExpansionTarget(currentZoom, Number(expansionZoom))
if (currentZoom >= 21.75 && targetZoom <= 22) {
const count = Number(
feature.properties?.total_count || feature.properties?.point_count
) || 1
const label = (state.element.dataset.clusterLabel || "%{count} items")
.replace("%{count}", new Intl.NumberFormat(uiLocale()).format(count))
new maplibregl.Popup({offset: 18})
.setLngLat(feature.geometry.coordinates)
.setDOMContent(state.popupContent({
title: label,
location: state.element.dataset.clusterMaxZoomLabel || ""
}))
.addTo(state.map)
return
}
state.clusterExpansionPending = true
state.map.easeTo({
center: feature.geometry.coordinates,
zoom: targetZoom,
duration: 300
})
})
}
state.expandServerCluster = feature => {
if (!feature || !state.map) return
const count = Number(feature.properties?.weight) || 1
const requestedZoom = Number(feature.properties?.expansion_zoom)
const currentZoom = state.map.getZoom()
const expansionZoom = clusterExpansionTarget(
currentZoom,
Number.isFinite(requestedZoom) ? requestedZoom : currentZoom + 2
)
if (currentZoom >= 21.75 && expansionZoom <= 22) {
const label = (state.element.dataset.clusterLabel || "%{count} items")
.replace("%{count}", String(count))
new maplibregl.Popup({offset: 18})
.setLngLat(feature.geometry.coordinates)
.setDOMContent(state.popupContent({
title: label,
location: state.element.dataset.clusterMaxZoomLabel || ""
}))
.addTo(state.map)
return
}
state.clusterExpansionPending = true
state.map.easeTo({
center: feature.geometry.coordinates,
zoom: expansionZoom,
duration: 300
})
}
state.syncClusterMarkers = () => {
state.markers.forEach(marker => marker.remove())
state.markers = []
if (
!state.map ||
state.element.dataset.discoveryMap !== "true" ||
!state.map.getLayer(state.clusterCircleId) ||
!state.map.getLayer(state.serverClusterCircleId)
) return
const features = state.map.queryRenderedFeatures({
layers: [state.clusterCircleId, state.serverClusterCircleId]
})
const seen = new Set()
features.forEach(feature => {
const nativeCluster =
feature.properties?.cluster === true || feature.properties?.cluster === "true"
const key = nativeCluster
? `native:${feature.properties.cluster_id}`
: `server:${feature.properties?.server_cluster_id || feature.properties?.id}`
if (seen.has(key)) return
seen.add(key)
const count = Number(
nativeCluster
? feature.properties?.total_count || feature.properties?.point_count
: feature.properties?.weight
) || 1
const urgentCount = Number(
nativeCluster
? feature.properties?.urgent_count
: feature.properties?.urgent_weight
) || 0
const button = document.createElement("button")
button.type = "button"
button.className = `request-map-cluster ${clusterSizeClass(count)}`
button.dataset.clusterCount = String(count)
button.classList.toggle("is-urgent", urgentCount > 0)
button.textContent = clusterCountLabel(count, uiLocale())
const formattedCount = new Intl.NumberFormat(uiLocale()).format(count)
button.setAttribute(
"aria-label",
(state.element.dataset.clusterLabel || "%{count} items")
.replace("%{count}", formattedCount)
)
button.title = button.getAttribute("aria-label")
button.addEventListener("click", event => {
event.stopPropagation()
if (nativeCluster) {
state.expandNativeCluster(feature)
} else {
state.expandServerCluster(feature)
}
})
state.markers.push(
new maplibregl.Marker({element: button})
.setLngLat(feature.geometry.coordinates)
.addTo(state.map)
)
})
}
state.syncVisibleAreas = () => {
if (!state.map) return
const areaSource = state.map.getSource(state.areaSourceId)
if (!areaSource) return
let visiblePoints = state.points
if (state.element.dataset.discoveryMap === "true") {
if (!state.map.getSource(state.clusterSourceId)?.loaded()) return
const visibleIds = new Set(
state.map
.querySourceFeatures(state.clusterSourceId, {
filter: [
"all",
["!", ["has", "point_count"]],
["!=", ["get", "item_type"], "cluster"]
]
})
.map(feature => feature.properties?.id)
.filter(Boolean)
)
visiblePoints = state.points.filter(point => visibleIds.has(point.id))
}
const signature = visiblePoints
.filter(point => !point.exact && Number.isFinite(point.radius_meters))
.map(point => point.id)
.sort()
.join(":")
if (signature === state.visibleAreaSignature) return
state.visibleAreaSignature = signature
areaSource.setData(areaFeatureCollection(visiblePoints))
}
state.renderMarkers = () => {
if (!state.map) return
@ -525,22 +696,45 @@ const createAidMap = element => {
state.markerElements.clear()
const points = markerPoints(state.element)
const displayPoints = mergeNearbyMapPoints(state.map, points)
const exactPoints = displayPoints.filter(
point => point.type !== "cluster" && (point.exact || !Number.isFinite(point.radius_meters))
)
const clusterPoints = displayPoints.filter(point => point.type === "cluster")
const areaFeatures = areaFeatureCollection(displayPoints)
const clusterFeatures = clusterFeatureCollection(clusterPoints)
state.points = points
const pointFeatures = pointFeatureCollection(points)
const areaFeatures =
state.element.dataset.discoveryMap === "true"
? emptyFeatureCollection()
: areaFeatureCollection(points)
const bounds = new maplibregl.LngLatBounds()
exactPoints.forEach(point => bounds.extend([point.longitude, point.latitude]))
areaFeatures.features.forEach(feature => extendBoundsWithFeature(bounds, feature))
clusterFeatures.features.forEach(feature => bounds.extend(feature.geometry.coordinates))
state.locationBounds = displayPoints.length > 0 ? bounds : null
points.forEach(point => {
const clusterBounds = point.bounds
if (state.map.isStyleLoaded()) {
const areaSource = state.map.getSource(state.areaSourceId)
if (
clusterBounds &&
[clusterBounds.west, clusterBounds.south, clusterBounds.east, clusterBounds.north]
.every(value => Number.isFinite(Number(value)))
) {
bounds.extend([Number(clusterBounds.west), Number(clusterBounds.south)])
bounds.extend([Number(clusterBounds.east), Number(clusterBounds.north)])
} else {
bounds.extend([point.longitude, point.latitude])
}
})
state.locationBounds = points.length > 0 ? bounds : null
const existingAreaSource = state.map.getSource(state.areaSourceId)
const existingPointSource = state.map.getSource(state.clusterSourceId)
if (
(!existingAreaSource || !existingPointSource) &&
!state.map.isStyleLoaded()
) {
state.pendingMarkerRender = true
return
}
state.pendingMarkerRender = false
{
const areaSource = existingAreaSource
if (areaSource) {
areaSource.setData(areaFeatures)
@ -550,12 +744,14 @@ const createAidMap = element => {
id: state.areaFillId,
type: "fill",
source: state.areaSourceId,
minzoom: state.element.dataset.discoveryMap === "true" ? 12 : 0,
paint: {"fill-color": "#278467", "fill-opacity": 0.18}
})
state.map.addLayer({
id: state.areaLineId,
type: "line",
source: state.areaSourceId,
minzoom: state.element.dataset.discoveryMap === "true" ? 12 : 0,
paint: {
"line-color": "#16725b",
"line-width": 2,
@ -566,6 +762,7 @@ const createAidMap = element => {
id: state.areaSelectedId,
type: "line",
source: state.areaSourceId,
minzoom: state.element.dataset.discoveryMap === "true" ? 12 : 0,
filter: ["==", ["get", "id"], ""],
paint: {"line-color": "#d6573b", "line-width": 5}
})
@ -588,83 +785,171 @@ const createAidMap = element => {
})
}
}
const pointSource = existingPointSource
exactPoints.forEach(point => {
const popup = new maplibregl.Popup({offset: 18}).setDOMContent(state.popupContent(point))
const markerColor = point.type === "live" ? "#2563eb" : point.exact ? "#d6573b" : "#278467"
const marker = new maplibregl.Marker({color: markerColor})
.setLngLat([point.longitude, point.latitude])
.setPopup(popup)
.addTo(state.map)
if (pointSource) {
pointSource.setData(pointFeatures)
} else {
const sourceOptions = {
type: "geojson",
data: pointFeatures,
generateId: false
}
const markerElement = marker.getElement()
markerElement.dataset.markerType = point.type || "request"
markerElement.setAttribute("aria-label", point.title || uiText("mapMarkerLabel"))
if (point.id) {
markerElement.dataset.requestId = point.id
markerElement.addEventListener("click", () => state.selectRequest(point.id))
state.markerElements.set(point.id, markerElement)
}
state.markers.push(marker)
})
clusterPoints.forEach(point => {
const element = document.createElement("button")
element.type = "button"
element.className = `request-map-cluster ${clusterSizeClass(point.count)}`
element.dataset.clusterCount = String(point.count)
element.classList.toggle("is-urgent", Number(point.urgent_count) > 0)
element.textContent = String(point.count)
element.setAttribute(
"aria-label",
(state.element.dataset.clusterLabel || "%{count} requests")
.replace("%{count}", String(point.count))
)
element.title = element.getAttribute("aria-label")
element.addEventListener("click", event => {
event.stopPropagation()
state.clusterExpansionPending = true
const bounds = point.bounds
const hasBounds =
bounds &&
[bounds.west, bounds.south, bounds.east, bounds.north]
.every(value => Number.isFinite(Number(value)))
const hasArea =
hasBounds &&
(Math.abs(Number(bounds.east) - Number(bounds.west)) > 0.000001 ||
Math.abs(Number(bounds.north) - Number(bounds.south)) > 0.000001)
if (hasArea) {
state.map.fitBounds(
[
[Number(bounds.west), Number(bounds.south)],
[Number(bounds.east), Number(bounds.north)]
],
{
padding: 72,
maxZoom: Math.min(state.map.getZoom() + 5, 18),
duration: 300
if (state.element.dataset.discoveryMap === "true") {
Object.assign(sourceOptions, {
maxzoom: 22,
cluster: true,
clusterRadius: 64,
clusterMaxZoom: 21,
clusterMinPoints: 2,
clusterProperties: {
total_count: ["+", ["get", "weight"]],
urgent_count: ["+", ["get", "urgent_weight"]]
}
)
} else {
state.map.easeTo({
center: [point.longitude, point.latitude],
zoom: Math.min(state.map.getZoom() + 2, 20),
duration: 300
})
}
})
const marker = new maplibregl.Marker({element})
.setLngLat([point.longitude, point.latitude])
.addTo(state.map)
state.map.addSource(state.clusterSourceId, sourceOptions)
state.map.addLayer({
id: state.clusterCircleId,
type: "circle",
source: state.clusterSourceId,
filter: ["has", "point_count"],
paint: {
"circle-color": [
"case",
[">", ["coalesce", ["get", "urgent_count"], 0], 0],
"#c2412d",
"#087f5b"
],
"circle-radius": [
"step",
["coalesce", ["get", "total_count"], ["get", "point_count"]],
22,
100,
25,
1_000,
28,
10_000,
31,
100_000,
34
],
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 3,
"circle-opacity": 0.96
}
})
state.map.addLayer({
id: state.serverClusterCircleId,
type: "circle",
source: state.clusterSourceId,
filter: [
"all",
["!", ["has", "point_count"]],
["==", ["get", "item_type"], "cluster"]
],
paint: {
"circle-color": [
"case",
[">", ["coalesce", ["get", "urgent_weight"], 0], 0],
"#c2412d",
"#087f5b"
],
"circle-radius": [
"step",
["get", "weight"],
22,
100,
25,
1_000,
28,
10_000,
31,
100_000,
34
],
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 3,
"circle-opacity": 0.96
}
})
state.map.addLayer({
id: state.pointCircleId,
type: "circle",
source: state.clusterSourceId,
filter: [
"all",
["!", ["has", "point_count"]],
["!=", ["get", "item_type"], "cluster"]
],
paint: {
"circle-color": [
"case",
["==", ["get", "item_type"], "live"],
"#2563eb",
["==", ["get", "exact"], true],
"#d6573b",
"#278467"
],
"circle-radius": 9,
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 3
}
})
state.map.addLayer({
id: state.pointSelectedId,
type: "circle",
source: state.clusterSourceId,
filter: ["==", ["get", "id"], state.selectedItemId || ""],
paint: {
"circle-color": "rgba(0,0,0,0)",
"circle-radius": 15,
"circle-stroke-color": "#111827",
"circle-stroke-width": 3
}
})
state.markers.push(marker)
})
const interactiveLayers = [
state.clusterCircleId,
state.serverClusterCircleId,
state.pointCircleId
]
interactiveLayers.forEach(layerId => {
state.map.on("mouseenter", layerId, () => {
state.map.getCanvas().style.cursor = "pointer"
})
state.map.on("mouseleave", layerId, () => {
state.map.getCanvas().style.cursor = ""
})
})
state.map.on("click", state.clusterCircleId, event => {
const feature = event.features?.[0]
if (feature) state.expandNativeCluster(feature)
})
state.map.on("click", state.serverClusterCircleId, event => {
const feature = event.features?.[0]
if (feature) state.expandServerCluster(feature)
})
state.map.on("click", state.pointCircleId, event => {
const feature = event.features?.[0]
const properties = feature?.properties
if (!feature || !properties) return
if (properties.id) state.selectRequest(properties.id)
new maplibregl.Popup({offset: 18})
.setLngLat(feature.geometry.coordinates)
.setDOMContent(state.popupContent(properties))
.addTo(state.map)
})
}
}
if (state.element.dataset.autoFit !== "false") state.fitMarkers()
}
@ -752,7 +1037,6 @@ const createAidMap = element => {
state.onMoveEnd = () => {
if (!state.initializedViewport || state.element.dataset.discoveryMap !== "true") return
state.renderMarkers()
const viewport = mapViewport(state.map)
if (!viewport) return
@ -764,11 +1048,23 @@ const createAidMap = element => {
}
}
state.onIdle = () => {
state.markReady()
if (state.pendingMarkerRender) {
state.renderMarkers()
if (state.pendingMarkerRender) return
}
state.syncVisibleAreas()
state.syncClusterMarkers()
}
state.onRequestHighlight = event => state.highlightRequest(event.detail?.id)
state.map.on("dataloading", state.markLoading)
state.map.on("load", state.onLoad)
state.map.on("idle", state.markReady)
state.map.on("idle", state.onIdle)
state.map.on("moveend", state.onMoveEnd)
window.addEventListener("wnh:request-highlight", state.onRequestHighlight)
if ("ResizeObserver" in window) {
@ -785,7 +1081,7 @@ const createAidMap = element => {
state.resizeObserver?.disconnect()
state.map?.off("dataloading", state.markLoading)
state.map?.off("load", state.onLoad)
state.map?.off("idle", state.markReady)
state.map?.off("idle", state.onIdle)
state.map?.off("moveend", state.onMoveEnd)
window.removeEventListener("wnh:request-highlight", state.onRequestHighlight)
state.map?.remove()

View File

@ -0,0 +1,59 @@
const finiteCount = (value, fallback) => {
const count = Number(value)
return Number.isFinite(count) && count >= 0 ? count : fallback
}
export const clusterSizeClass = count => {
if (count >= 100_000) return "is-xxl"
if (count >= 10_000) return "is-xl"
if (count >= 1_000) return "is-lg"
return "is-md"
}
export const clusterCountLabel = (count, locale = "en") => {
const value = finiteCount(count, 0)
if (value < 1_000) return new Intl.NumberFormat(locale).format(value)
return new Intl.NumberFormat(locale, {
notation: "compact",
maximumFractionDigits: value < 10_000 ? 1 : 0
}).format(value)
}
export const clusterExpansionTarget = (currentZoom, requestedZoom, maximumZoom = 22) => {
const current = finiteCount(currentZoom, 0)
const requested = finiteCount(requestedZoom, current + 1)
const nearestIntegerZoom = Math.round(current)
const normalizedCurrent =
Math.abs(current - nearestIntegerZoom) < 0.001 ? nearestIntegerZoom : current
const nextIntegerZoom = Math.floor(normalizedCurrent) + 1
return Math.min(maximumZoom, Math.max(nextIntegerZoom, requested))
}
export const pointFeatureCollection = points => ({
type: "FeatureCollection",
features: points.map(point => ({
type: "Feature",
id: point.id,
properties: {
id: point.id,
item_type: point.type || "request",
title: point.title || "",
location: point.location || "",
exact: point.exact === true,
radius_meters: Number.isFinite(point.radius_meters) ? point.radius_meters : 0,
weight: finiteCount(point.count, 1),
urgent_weight: finiteCount(point.urgent_count, 0),
server_cluster_id: point.cluster_id || "",
parent_cluster_id: point.parent_cluster_id || "",
hierarchy_level: finiteCount(point.hierarchy_level, 0),
expansion_zoom: finiteCount(point.expansion_zoom, 22)
},
geometry: {
type: "Point",
coordinates: [Number(point.longitude), Number(point.latitude)]
}
}))
})

View File

@ -0,0 +1,123 @@
import assert from "node:assert/strict"
import test from "node:test"
import {Supercluster} from "@maplibre/geojson-vt"
import {
clusterCountLabel,
clusterExpansionTarget,
clusterSizeClass,
pointFeatureCollection
} from "./map_cluster_features.mjs"
const buildIndex = points => {
const index = new Supercluster({
radius: 64,
maxZoom: 21,
minPoints: 2,
map: properties => ({
total_count: properties.weight,
urgent_count: properties.urgent_weight
}),
reduce: (accumulated, properties) => {
accumulated.total_count += properties.total_count
accumulated.urgent_count += properties.urgent_count
}
})
index.load(pointFeatureCollection(points).features)
return index
}
test("weighted server nodes preserve the represented record total", () => {
const index = buildIndex([
{id: "request:4:1:1", type: "cluster", longitude: 30.50, latitude: 50.45, count: 700},
{
id: "request:4:1:2",
type: "cluster",
longitude: 30.51,
latitude: 50.46,
count: 300,
urgent_count: 10
},
{id: "one", type: "request", longitude: -74.0, latitude: 40.7}
])
const world = index.getClusters([-180, -85, 180, 85], 0)
const represented = world.reduce(
(sum, feature) => sum + (feature.properties.total_count || feature.properties.weight),
0
)
const urgent = world.reduce(
(sum, feature) => sum + (feature.properties.urgent_count || feature.properties.urgent_weight),
0
)
assert.equal(represented, 1_001)
assert.equal(urgent, 10)
})
test("server hierarchy metadata survives GeoJSON conversion without becoming a native cluster", () => {
const feature = pointFeatureCollection([
{
id: "request:11:1197:1378",
type: "cluster",
cluster_id: "request:11:1197:1378",
parent_cluster_id: "request:10:598:689",
hierarchy_level: 11,
expansion_zoom: 13,
longitude: 30.52,
latitude: 50.45,
count: 27
}
]).features[0]
assert.equal(feature.properties.server_cluster_id, "request:11:1197:1378")
assert.equal(feature.properties.parent_cluster_id, "request:10:598:689")
assert.equal(feature.properties.hierarchy_level, 11)
assert.equal(feature.properties.expansion_zoom, 13)
assert.equal(feature.properties.weight, 27)
assert.equal("point_count" in feature.properties, false)
})
test("native clusters expose an expansion zoom and eventually split unique coordinates", () => {
const index = buildIndex([
{id: "a", longitude: 30.52340, latitude: 50.45010},
{id: "b", longitude: 30.52341, latitude: 50.45011},
{id: "c", longitude: 30.60, latitude: 50.50}
])
const world = index.getClusters([-180, -85, 180, 85], 0)
const cluster = world.find(feature => feature.properties.cluster)
assert.ok(cluster)
assert.ok(index.getClusterExpansionZoom(cluster.properties.cluster_id) > 0)
assert.equal(
index.getClusters([30.5, 50.4, 30.7, 50.6], 22)
.filter(feature => !feature.properties.cluster)
.length,
3
)
})
test("cluster size classes remain stable for accessible count buttons", () => {
assert.equal(clusterSizeClass(999), "is-md")
assert.equal(clusterSizeClass(1_000), "is-lg")
assert.equal(clusterSizeClass(10_000), "is-xl")
assert.equal(clusterSizeClass(100_000), "is-xxl")
})
test("large visual counts are compact while exact values remain available to accessibility text", () => {
assert.equal(clusterCountLabel(999, "en"), "999")
assert.equal(clusterCountLabel(1_250, "en"), "1.3K")
assert.equal(clusterCountLabel(48_533, "en"), "49K")
assert.equal(clusterCountLabel(1_000_000, "en"), "1M")
})
test("cluster expansion always advances at least one integer zoom level", () => {
assert.equal(clusterExpansionTarget(9, 9), 10)
assert.equal(clusterExpansionTarget(8.999_999_096_835_52, 9), 10)
assert.equal(clusterExpansionTarget(10.999_994_193_019_71, 11), 12)
assert.equal(clusterExpansionTarget(9.6, 10), 10)
assert.equal(clusterExpansionTarget(9.6, 13), 13)
assert.equal(clusterExpansionTarget(21.9, 24), 22)
})

View File

@ -1,147 +0,0 @@
const clusterCount = point => {
const count = Number(point?.count)
return Number.isFinite(count) && count > 0 ? count : 1
}
const urgentCount = point => {
const count = Number(point?.urgent_count)
return Number.isFinite(count) && count > 0 ? count : 0
}
export const clusterDiameterPixels = count => {
if (count >= 100_000) return 60
if (count >= 10_000) return 56
if (count >= 1_000) return 52
return 44
}
export const clusterSizeClass = count => {
if (count >= 100_000) return "is-xxl"
if (count >= 10_000) return "is-xl"
if (count >= 1_000) return "is-lg"
return "is-md"
}
const mergeDistance = (left, right, gap) =>
(clusterDiameterPixels(left.count) + clusterDiameterPixels(right.count)) / 2 + gap
const coordinatesFromPixel = (map, pixel) => {
const coordinates = map.unproject([pixel.x, pixel.y])
return {
longitude: Number(coordinates.lng ?? coordinates[0]),
latitude: Number(coordinates.lat ?? coordinates[1])
}
}
const finiteCoordinate = value => {
const coordinate = Number(value)
return Number.isFinite(coordinate) ? coordinate : null
}
const pointBounds = point => {
const longitude = finiteCoordinate(point?.longitude)
const latitude = finiteCoordinate(point?.latitude)
const source = point?.bounds || {}
const west = finiteCoordinate(source.west) ?? longitude
const south = finiteCoordinate(source.south) ?? latitude
const east = finiteCoordinate(source.east) ?? longitude
const north = finiteCoordinate(source.north) ?? latitude
if ([west, south, east, north].some(value => value === null)) return null
return {
west: Math.min(west, east),
south: Math.min(south, north),
east: Math.max(west, east),
north: Math.max(south, north)
}
}
const groupBounds = points => {
const bounds = points.map(pointBounds).filter(Boolean)
if (bounds.length === 0) return null
return bounds.reduce(
(result, current) => ({
west: Math.min(result.west, current.west),
south: Math.min(result.south, current.south),
east: Math.max(result.east, current.east),
north: Math.max(result.north, current.north)
}),
bounds[0]
)
}
const nearbyMapPointGroups = (map, points, options = {}) => {
const gap = Number.isFinite(options.gap) ? Math.max(options.gap, 0) : 8
const groups = points.map(point => {
const pixel = map.project([point.longitude, point.latitude])
return {
points: [point],
x: pixel.x,
y: pixel.y,
count: clusterCount(point),
urgent_count: urgentCount(point)
}
})
let merged = true
while (merged) {
merged = false
outer:
for (let leftIndex = 0; leftIndex < groups.length; leftIndex += 1) {
for (let rightIndex = leftIndex + 1; rightIndex < groups.length; rightIndex += 1) {
const left = groups[leftIndex]
const right = groups[rightIndex]
const distance = Math.hypot(left.x - right.x, left.y - right.y)
if (distance >= mergeDistance(left, right, gap)) continue
const count = left.count + right.count
left.x = (left.x * left.count + right.x * right.count) / count
left.y = (left.y * left.count + right.y * right.count) / count
left.count = count
left.urgent_count += right.urgent_count
left.points.push(...right.points)
groups.splice(rightIndex, 1)
merged = true
break outer
}
}
}
return groups
}
const visualCluster = (map, group) => {
const coordinates = coordinatesFromPixel(map, group)
const firstId = group.points[0]?.id || "anonymous"
return {
type: "cluster",
id: `visual-cluster-${group.points.length}-${firstId}`,
count: group.count,
urgent_count: group.urgent_count,
longitude: coordinates.longitude,
latitude: coordinates.latitude,
bounds: groupBounds(group.points)
}
}
export const mergeNearbyMapClusters = (map, points, options = {}) =>
nearbyMapPointGroups(map, points, options).map(group => visualCluster(map, group))
export const mergeNearbyMapPoints = (map, points, options = {}) => {
return nearbyMapPointGroups(map, points, options).map(group => {
const point = group.points[0]
if (group.points.length === 1 && point?.type !== "cluster") return point
return visualCluster(map, group)
})
}

View File

@ -1,101 +0,0 @@
import assert from "node:assert/strict"
import test from "node:test"
import {
clusterDiameterPixels,
clusterSizeClass,
mergeNearbyMapClusters,
mergeNearbyMapPoints
} from "./map_cluster_points.mjs"
const flatMap = scale => ({
project: ([longitude, latitude]) => ({x: longitude * scale, y: latitude * scale}),
unproject: ([x, y]) => ({lng: x / scale, lat: y / scale})
})
test("merges visually overlapping clusters and preserves their total", () => {
const points = [
{
id: "a",
type: "cluster",
count: 100,
urgent_count: 2,
longitude: 0,
latitude: 0,
bounds: {west: -5, south: -2, east: 5, north: 2}
},
{
id: "b",
type: "cluster",
count: 50,
urgent_count: 1,
longitude: 30,
latitude: 0,
bounds: {west: 25, south: -3, east: 35, north: 4}
},
{id: "c", type: "cluster", count: 7, urgent_count: 0, longitude: 140, latitude: 0}
]
const result = mergeNearbyMapClusters(flatMap(1), points)
assert.equal(result.length, 2)
assert.equal(result.reduce((total, cluster) => total + cluster.count, 0), 157)
const merged = result.find(cluster => cluster.count === 150)
assert.equal(merged.urgent_count, 3)
assert.equal(merged.longitude, 10)
assert.equal(merged.latitude, 0)
assert.deepEqual(merged.bounds, {west: -5, south: -3, east: 35, north: 4})
})
test("splits the same clusters again at a closer zoom", () => {
const points = [
{id: "a", type: "cluster", count: 20, longitude: 0, latitude: 0},
{id: "b", type: "cluster", count: 30, longitude: 30, latitude: 0}
]
assert.equal(mergeNearbyMapClusters(flatMap(1), points).length, 1)
assert.equal(mergeNearbyMapClusters(flatMap(3), points).length, 2)
})
test("merges a singleton into an overlapping cluster at a distant zoom", () => {
const points = [
{id: "cluster", type: "cluster", count: 351, longitude: 0, latitude: 0},
{id: "request", type: "request", longitude: 28, latitude: 0, exact: true}
]
const result = mergeNearbyMapPoints(flatMap(1), points)
assert.equal(result.length, 1)
assert.equal(result[0].type, "cluster")
assert.equal(result[0].count, 352)
assert.deepEqual(result[0].bounds, {west: 0, south: 0, east: 28, north: 0})
})
test("keeps separated singleton points interactive at a closer zoom", () => {
const points = [
{id: "left", type: "request", longitude: 0, latitude: 0, exact: true},
{
id: "right",
type: "request",
longitude: 30,
latitude: 0,
radius_meters: 1_000
}
]
const result = mergeNearbyMapPoints(flatMap(3), points)
assert.deepEqual(result, points)
})
test("uses larger non-overlapping markers for larger totals", () => {
assert.equal(clusterDiameterPixels(999), 44)
assert.equal(clusterDiameterPixels(1_000), 52)
assert.equal(clusterDiameterPixels(10_000), 56)
assert.equal(clusterDiameterPixels(100_000), 60)
assert.equal(clusterSizeClass(999), "is-md")
assert.equal(clusterSizeClass(1_000), "is-lg")
assert.equal(clusterSizeClass(10_000), "is-xl")
assert.equal(clusterSizeClass(100_000), "is-xxl")
})

View File

@ -11,7 +11,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
@default_rows 1_000_000
@synthetic_users 1_000
@viewer_email "scale-viewer@example.invalid"
@fixture_version 7
@fixture_version 8
@request_open_modulus 1
@activity_open_modulus 1
@places [
@ -425,10 +425,11 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
'Synthetic data only; no real-world action is requested.',
jsonb_build_object(
'scale_fixture', true,
'scale_fixture_version', $7::integer
'scale_fixture_version', $7::integer,
'scale_place', place.name,
'scale_location_index', local.local_index
),
place.name || ' synthetic area ' ||
((variation.category_key + variation.time_key) % 100 + 1),
place.name || ' synthetic location ' || (local.local_index + 1),
CASE WHEN variation.visibility_key % 20 = 0 THEN NULL
ELSE ST_SetSRID(
ST_MakePoint(coordinates.longitude, coordinates.latitude),
@ -469,8 +470,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
FROM generate_series(1, $1) AS value
CROSS JOIN LATERAL (
SELECT
md5('wnh-scale-request-v5-' || value) AS fixture_hash,
md5('wnh-scale-request-geography-v5-' || value) AS geography_hash
md5('wnh-scale-request-v8-' || value) AS fixture_hash
) AS fixture
CROSS JOIN LATERAL (
SELECT
@ -482,52 +482,56 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
AS time_key,
('x' || substr(fixture.fixture_hash, 25, 8))::bit(32)::bigint
AS category_key,
('x' || substr(fixture.geography_hash, 1, 8))::bit(32)::bigint
AS place_key,
('x' || substr(fixture.geography_hash, 9, 8))::bit(32)::bigint
AS angle_key,
('x' || substr(fixture.geography_hash, 17, 8))::bit(32)::bigint
AS distance_key,
('x' || substr(fixture.geography_hash, 25, 8))::bit(32)::bigint
AS owner_key
('x' || substr(md5('wnh-scale-request-owner-v8-' || value), 1, 8))
::bit(32)::bigint AS owner_key
) AS variation
CROSS JOIN LATERAL (
SELECT name, longitude, latitude
SELECT name, longitude, latitude, position
FROM unnest(
$4::text[],
$5::double precision[],
$6::double precision[]
) WITH ORDINALITY AS places(name, longitude, latitude, position)
WHERE places.position = variation.place_key % cardinality($4::text[]) + 1
WHERE places.position = ((value - 1) % cardinality($4::text[])) + 1
) AS place
CROSS JOIN LATERAL (
SELECT
2 * pi() *
((variation.angle_key % 1000003)::double precision / 1000003.0) AS angle,
sqrt(
((variation.distance_key / 20 % 1000003)::double precision + 1) /
1000004.0
) *
CASE WHEN variation.distance_key % 20 < 12 THEN 15.0
WHEN variation.distance_key % 20 < 18 THEN 60.0
ELSE 220.0
END AS distance_km
) AS geography
(value - 1) / cardinality($4::text[]) AS local_index
) AS local
CROSS JOIN LATERAL (
SELECT
place.latitude +
geography.distance_km / 111.32 * sin(geography.angle) AS raw_latitude,
(
((local.local_index % 256) * 73 +
((local.local_index / 256) % 256) * 16) % 256
)::double precision AS grid_x,
(
(((local.local_index / 256) % 256) * 53 +
(local.local_index % 256) * 29) % 256
)::double precision AS grid_y
) AS grid
CROSS JOIN LATERAL (
SELECT
(grid.grid_x / 255.0 - 0.5) * 12.0 AS east_km,
(grid.grid_y / 255.0 - 0.5) * 12.0 AS north_km,
radians((place.position * 137.50776405) % 360.0) AS rotation
) AS local_offset
CROSS JOIN LATERAL (
SELECT
local_offset.east_km * cos(local_offset.rotation) -
local_offset.north_km * sin(local_offset.rotation) AS east_km,
local_offset.east_km * sin(local_offset.rotation) +
local_offset.north_km * cos(local_offset.rotation) AS north_km
) AS rotated_offset
CROSS JOIN LATERAL (
SELECT
greatest(
-84.0,
least(84.0, place.latitude + rotated_offset.north_km / 111.32)
) AS latitude,
place.longitude +
geography.distance_km /
(111.32 * greatest(abs(cos(radians(place.latitude))), 0.2)) *
cos(geography.angle) AS raw_longitude
) AS raw_coordinates
CROSS JOIN LATERAL (
SELECT
greatest(-84.0, least(84.0, raw_coordinates.raw_latitude)) AS latitude,
(raw_coordinates.raw_longitude + 180.0) -
360.0 * floor((raw_coordinates.raw_longitude + 180.0) / 360.0) -
180.0 AS longitude
rotated_offset.east_km /
(111.32 * greatest(abs(cos(radians(place.latitude))), 0.2))
AS longitude
) AS coordinates
ON CONFLICT (id) DO UPDATE SET
title = EXCLUDED.title,
@ -578,10 +582,11 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
'Synthetic scale activity used to test geographic discovery, privacy, and UI.',
jsonb_build_object(
'scale_fixture', true,
'scale_fixture_version', $7::integer
'scale_fixture_version', $7::integer,
'scale_place', place.name,
'scale_location_index', local.local_index
),
place.name || ' synthetic area ' ||
((variation.category_key + variation.time_key) % 100 + 1),
place.name || ' synthetic location ' || (local.local_index + 1),
ST_SetSRID(
ST_MakePoint(coordinates.longitude, coordinates.latitude),
4326
@ -615,8 +620,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
FROM generate_series(1, $1) AS value
CROSS JOIN LATERAL (
SELECT
md5('wnh-scale-activity-v5-' || value) AS fixture_hash,
md5('wnh-scale-activity-geography-v5-' || value) AS geography_hash
md5('wnh-scale-activity-v8-' || value) AS fixture_hash
) AS fixture
CROSS JOIN LATERAL (
SELECT
@ -628,52 +632,56 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
AS time_key,
('x' || substr(fixture.fixture_hash, 25, 8))::bit(32)::bigint
AS category_key,
('x' || substr(fixture.geography_hash, 1, 8))::bit(32)::bigint
AS place_key,
('x' || substr(fixture.geography_hash, 9, 8))::bit(32)::bigint
AS angle_key,
('x' || substr(fixture.geography_hash, 17, 8))::bit(32)::bigint
AS distance_key,
('x' || substr(fixture.geography_hash, 25, 8))::bit(32)::bigint
AS owner_key
('x' || substr(md5('wnh-scale-activity-owner-v8-' || value), 1, 8))
::bit(32)::bigint AS owner_key
) AS variation
CROSS JOIN LATERAL (
SELECT name, longitude, latitude
SELECT name, longitude, latitude, position
FROM unnest(
$4::text[],
$5::double precision[],
$6::double precision[]
) WITH ORDINALITY AS places(name, longitude, latitude, position)
WHERE places.position = variation.place_key % cardinality($4::text[]) + 1
WHERE places.position = ((value - 1) % cardinality($4::text[])) + 1
) AS place
CROSS JOIN LATERAL (
SELECT
2 * pi() *
((variation.angle_key % 1000003)::double precision / 1000003.0) AS angle,
sqrt(
((variation.distance_key / 20 % 1000003)::double precision + 1) /
1000004.0
) *
CASE WHEN variation.distance_key % 20 < 12 THEN 15.0
WHEN variation.distance_key % 20 < 18 THEN 60.0
ELSE 220.0
END AS distance_km
) AS geography
(value - 1) / cardinality($4::text[]) AS local_index
) AS local
CROSS JOIN LATERAL (
SELECT
place.latitude +
geography.distance_km / 111.32 * sin(geography.angle) AS raw_latitude,
(
((local.local_index % 256) * 73 +
((local.local_index / 256) % 256) * 16 + 31) % 256
)::double precision AS grid_x,
(
(((local.local_index / 256) % 256) * 53 +
(local.local_index % 256) * 29 + 47) % 256
)::double precision AS grid_y
) AS grid
CROSS JOIN LATERAL (
SELECT
(grid.grid_x / 255.0 - 0.5) * 12.0 AS east_km,
(grid.grid_y / 255.0 - 0.5) * 12.0 AS north_km,
radians((place.position * 137.50776405 + 19.0) % 360.0) AS rotation
) AS local_offset
CROSS JOIN LATERAL (
SELECT
local_offset.east_km * cos(local_offset.rotation) -
local_offset.north_km * sin(local_offset.rotation) AS east_km,
local_offset.east_km * sin(local_offset.rotation) +
local_offset.north_km * cos(local_offset.rotation) AS north_km
) AS rotated_offset
CROSS JOIN LATERAL (
SELECT
greatest(
-84.0,
least(84.0, place.latitude + rotated_offset.north_km / 111.32)
) AS latitude,
place.longitude +
geography.distance_km /
(111.32 * greatest(abs(cos(radians(place.latitude))), 0.2)) *
cos(geography.angle) AS raw_longitude
) AS raw_coordinates
CROSS JOIN LATERAL (
SELECT
greatest(-84.0, least(84.0, raw_coordinates.raw_latitude)) AS latitude,
(raw_coordinates.raw_longitude + 180.0) -
360.0 * floor((raw_coordinates.raw_longitude + 180.0) / 360.0) -
180.0 AS longitude
rotated_offset.east_km /
(111.32 * greatest(abs(cos(radians(place.latitude))), 0.2))
AS longitude
) AS coordinates
ON CONFLICT (id) DO UPDATE SET
title = EXCLUDED.title,
@ -1125,7 +1133,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
defp fixture_place_count!("help_requests") do
scalar!("""
SELECT count(DISTINCT regexp_replace(location_label, ' synthetic area [0-9]+$', ''))
SELECT count(DISTINCT regexp_replace(location_label, ' synthetic location [0-9]+$', ''))
FROM help_requests
WHERE structured_data @> '{"scale_fixture": true}'::jsonb
""")
@ -1133,7 +1141,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
defp fixture_place_count!("activities") do
scalar!("""
SELECT count(DISTINCT regexp_replace(location_label, ' synthetic area [0-9]+$', ''))
SELECT count(DISTINCT regexp_replace(location_label, ' synthetic location [0-9]+$', ''))
FROM activities
WHERE structured_data @> '{"scale_fixture": true}'::jsonb
""")
@ -1146,7 +1154,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
SELECT count(*) AS place_rows
FROM help_requests
WHERE structured_data @> '{"scale_fixture": true}'::jsonb
GROUP BY regexp_replace(location_label, ' synthetic area [0-9]+$', '')
GROUP BY regexp_replace(location_label, ' synthetic location [0-9]+$', '')
) AS grouped_places
""")
end
@ -1158,7 +1166,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
SELECT count(*) AS place_rows
FROM activities
WHERE structured_data @> '{"scale_fixture": true}'::jsonb
GROUP BY regexp_replace(location_label, ' synthetic area [0-9]+$', '')
GROUP BY regexp_replace(location_label, ' synthetic location [0-9]+$', '')
) AS grouped_places
""")
end
@ -1173,7 +1181,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
AND status = 'open'
AND expires_at > now()
AND location_visibility <> 'hidden'
GROUP BY regexp_replace(location_label, ' synthetic area [0-9]+$', '')
GROUP BY regexp_replace(location_label, ' synthetic location [0-9]+$', '')
) AS grouped_places
""")
end
@ -1188,7 +1196,7 @@ defmodule Mix.Tasks.Wnh.ScaleFixtures do
AND status = 'open'
AND starts_at > now()
AND location_visibility = 'approximate_public'
GROUP BY regexp_replace(location_label, ' synthetic area [0-9]+$', '')
GROUP BY regexp_replace(location_label, ' synthetic location [0-9]+$', '')
) AS grouped_places
""")
end

View File

@ -10,7 +10,7 @@ defmodule WhoNeedHelp.Activities do
alias WhoNeedHelp.Activities.{Activity, Message, Participant}
alias WhoNeedHelp.Catalog
alias WhoNeedHelp.Catalog.Category
alias WhoNeedHelp.Help.DiscoveryViewport
alias WhoNeedHelp.Help.{DiscoveryCluster, DiscoveryViewport}
alias WhoNeedHelp.Pagination
alias WhoNeedHelp.Repo
alias WhoNeedHelp.Trust
@ -53,7 +53,9 @@ defmodule WhoNeedHelp.Activities do
end
def map_discovery_items(%Scope{user: user}, filters, %DiscoveryViewport{} = viewport) do
cell_size = DiscoveryViewport.cluster_cell_meters(viewport)
level = DiscoveryCluster.level_for_zoom(viewport.zoom)
cell_size = DiscoveryCluster.cell_size_meters(level)
world_half = DiscoveryCluster.world_half_meters()
points =
user
@ -87,65 +89,75 @@ defmodule WhoNeedHelp.Activities do
points
|> subquery()
|> group_by(
[point],
|> group_by([point], [
fragment(
"ST_SnapToGrid(ST_Transform(?, 3857), ?)",
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
fragment(
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
)
])
|> select([point], %{
count: count(point.id),
activity_id: fragment("CASE WHEN count(*) = 1 THEN min(?::text) END", point.id),
title: fragment("CASE WHEN count(*) = 1 THEN min(?) END", point.title),
location_label: fragment("CASE WHEN count(*) = 1 THEN min(?) END", point.location_label),
longitude:
fragment(
"CASE WHEN count(*) = 1 THEN min(ST_X(?)) ELSE avg(ST_X(?)) END",
point.public_location,
point.public_location
),
latitude:
fragment(
"CASE WHEN count(*) = 1 THEN min(ST_Y(?)) ELSE avg(ST_Y(?)) END",
point.public_location,
point.public_location
),
longitude: fragment("avg(ST_X(?))", point.public_location),
latitude: fragment("avg(ST_Y(?))", point.public_location),
west: fragment("min(ST_X(?))", point.public_location),
south: fragment("min(ST_Y(?))", point.public_location),
east: fragment("max(ST_X(?))", point.public_location),
north: fragment("max(ST_Y(?))", point.public_location),
west_m: fragment("min(ST_X(ST_Transform(?, 3857)))", point.public_location),
south_m: fragment("min(ST_Y(ST_Transform(?, 3857)))", point.public_location),
east_m: fragment("max(ST_X(ST_Transform(?, 3857)))", point.public_location),
north_m: fragment("max(ST_Y(ST_Transform(?, 3857)))", point.public_location),
cell_x:
fragment(
"ST_X(ST_SnapToGrid(ST_Transform(?, 3857), ?))",
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
cell_y:
fragment(
"ST_Y(ST_SnapToGrid(ST_Transform(?, 3857), ?))",
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
})
|> order_by(
[point],
|> order_by([point],
asc:
fragment(
"ST_X(ST_SnapToGrid(ST_Transform(?, 3857), ?))",
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
asc:
fragment(
"ST_Y(ST_SnapToGrid(ST_Transform(?, 3857), ?))",
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
)
|> Repo.all()
|> Enum.map(&map_discovery_item(&1, viewport.zoom))
|> Enum.map(&map_discovery_item(&1, level))
end
def count_map_discovery_items(%Scope{user: user}, filters) do
user
|> open_activities_query(filters, nil)
|> where([activity], activity.location_visibility == :approximate_public)
|> Repo.aggregate(:count, :id)
end
def list_my_activities(%Scope{user: user}) do
@ -968,10 +980,12 @@ defmodule WhoNeedHelp.Activities do
defp longitude_in_viewport?(longitude, %DiscoveryViewport{west: west, east: east}),
do: longitude >= west or longitude <= east
defp map_discovery_item(%{count: 1} = row, _zoom) do
defp map_discovery_item(%{count: 1} = row, level) do
%{
type: "activity",
id: row.activity_id,
parent_cluster_id: DiscoveryCluster.parent_id(:activity, level, row.cell_x, row.cell_y),
hierarchy_level: level,
title: row.title,
location: row.location_label,
latitude: row.latitude,
@ -980,15 +994,24 @@ defmodule WhoNeedHelp.Activities do
}
end
defp map_discovery_item(row, zoom) do
digest =
:crypto.hash(:sha256, "activity:#{zoom}:#{row.cell_x}:#{row.cell_y}")
|> Base.url_encode64(padding: false)
|> binary_part(0, 16)
defp map_discovery_item(row, level) do
cluster_id = DiscoveryCluster.cluster_id(:activity, level, row.cell_x, row.cell_y)
%{
type: "cluster",
id: "activity-cluster-#{digest}",
id: cluster_id,
cluster_id: cluster_id,
parent_cluster_id: DiscoveryCluster.parent_id(:activity, level, row.cell_x, row.cell_y),
hierarchy_level: level,
point_count: row.count,
expansion_zoom:
DiscoveryCluster.expansion_zoom(
level,
row.west_m,
row.south_m,
row.east_m,
row.north_m
),
count: row.count,
latitude: row.latitude,
longitude: row.longitude,

View File

@ -7,7 +7,7 @@ defmodule WhoNeedHelp.Help do
alias WhoNeedHelp.Accounts.Scope
alias WhoNeedHelp.Catalog
alias WhoNeedHelp.Catalog.Category
alias WhoNeedHelp.Help.{Assignment, DiscoveryViewport, HelpRequest}
alias WhoNeedHelp.Help.{Assignment, DiscoveryCluster, DiscoveryViewport, HelpRequest}
alias WhoNeedHelp.Pagination
alias WhoNeedHelp.ProductAnalytics
alias WhoNeedHelp.Push
@ -64,12 +64,17 @@ defmodule WhoNeedHelp.Help do
end
def map_discovery_items(%Scope{user: user}, filters, %DiscoveryViewport{} = viewport) do
cell_size = DiscoveryViewport.cluster_cell_meters(viewport)
level = DiscoveryCluster.level_for_zoom(viewport.zoom)
cell_size = DiscoveryCluster.cell_size_meters(level)
world_half = DiscoveryCluster.world_half_meters()
points =
user
|> open_requests_query(filters, nil)
|> where([request], not is_nil(request.location))
|> where(
[request],
not is_nil(request.location) and request.location_visibility != :hidden
)
|> filter_request_map_viewport(viewport)
|> select([request], %{
id: request.id,
@ -105,14 +110,20 @@ defmodule WhoNeedHelp.Help do
points
|> subquery()
|> group_by(
[point],
|> group_by([point], [
fragment(
"ST_SnapToGrid(ST_Transform(?, 3857), ?)",
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
fragment(
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
)
])
|> select([point], %{
count: count(point.id),
urgent_count: fragment("count(*) FILTER (WHERE ? = 'now')::bigint", point.urgency),
@ -125,52 +136,59 @@ defmodule WhoNeedHelp.Help do
point.location_visibility
),
radius_meters: fragment("CASE WHEN count(*) = 1 THEN max(?) END", point.radius_meters),
longitude:
fragment(
"CASE WHEN count(*) = 1 THEN min(ST_X(?)) ELSE avg(ST_X(?)) END",
point.public_location,
point.public_location
),
latitude:
fragment(
"CASE WHEN count(*) = 1 THEN min(ST_Y(?)) ELSE avg(ST_Y(?)) END",
point.public_location,
point.public_location
),
longitude: fragment("avg(ST_X(?))", point.public_location),
latitude: fragment("avg(ST_Y(?))", point.public_location),
west: fragment("min(ST_X(?))", point.public_location),
south: fragment("min(ST_Y(?))", point.public_location),
east: fragment("max(ST_X(?))", point.public_location),
north: fragment("max(ST_Y(?))", point.public_location),
west_m: fragment("min(ST_X(ST_Transform(?, 3857)))", point.public_location),
south_m: fragment("min(ST_Y(ST_Transform(?, 3857)))", point.public_location),
east_m: fragment("max(ST_X(ST_Transform(?, 3857)))", point.public_location),
north_m: fragment("max(ST_Y(ST_Transform(?, 3857)))", point.public_location),
cell_x:
fragment(
"ST_X(ST_SnapToGrid(ST_Transform(?, 3857), ?))",
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
cell_y:
fragment(
"ST_Y(ST_SnapToGrid(ST_Transform(?, 3857), ?))",
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
})
|> order_by(
[point],
|> order_by([point],
asc:
fragment(
"ST_X(ST_SnapToGrid(ST_Transform(?, 3857), ?))",
"floor((ST_X(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
),
asc:
fragment(
"ST_Y(ST_SnapToGrid(ST_Transform(?, 3857), ?))",
"floor((ST_Y(ST_Transform(?, 3857)) + ?) / ?)::bigint",
point.public_location,
constant(^world_half),
constant(^cell_size)
)
)
|> Repo.all()
|> Enum.map(&map_discovery_item(&1, viewport.zoom))
|> Enum.map(&map_discovery_item(&1, level))
end
def count_map_discovery_items(%Scope{user: user}, filters) do
user
|> open_requests_query(filters, nil)
|> where(
[request],
not is_nil(request.location) and request.location_visibility != :hidden
)
|> Repo.aggregate(:count, :id)
end
# Discovery lists include a privacy area's intersection with the viewport.
@ -1274,10 +1292,12 @@ defmodule WhoNeedHelp.Help do
where(query, ^condition)
end
defp map_discovery_item(%{count: 1} = row, _zoom) do
defp map_discovery_item(%{count: 1} = row, level) do
%{
type: "request",
id: row.request_id,
parent_cluster_id: DiscoveryCluster.parent_id(:request, level, row.cell_x, row.cell_y),
hierarchy_level: level,
title: row.title,
location: row.location_label,
latitude: row.latitude,
@ -1287,15 +1307,24 @@ defmodule WhoNeedHelp.Help do
}
end
defp map_discovery_item(row, zoom) do
digest =
:crypto.hash(:sha256, "#{zoom}:#{row.cell_x}:#{row.cell_y}")
|> Base.url_encode64(padding: false)
|> binary_part(0, 16)
defp map_discovery_item(row, level) do
cluster_id = DiscoveryCluster.cluster_id(:request, level, row.cell_x, row.cell_y)
%{
type: "cluster",
id: "cluster-#{digest}",
id: cluster_id,
cluster_id: cluster_id,
parent_cluster_id: DiscoveryCluster.parent_id(:request, level, row.cell_x, row.cell_y),
hierarchy_level: level,
point_count: row.count,
expansion_zoom:
DiscoveryCluster.expansion_zoom(
level,
row.west_m,
row.south_m,
row.east_m,
row.north_m
),
count: row.count,
urgent_count: row.urgent_count,
latitude: row.latitude,

View File

@ -0,0 +1,99 @@
defmodule WhoNeedHelp.Help.DiscoveryCluster do
@moduledoc """
Stable Web Mercator hierarchy used as the server-side input to MapLibre
Supercluster.
A browser cannot safely receive every discovery point once the data set
reaches millions of rows. The server therefore groups points into a fixed
quadtree. Unlike viewport-sized `ST_SnapToGrid` buckets, every cell has a
stable identity and every cell at level `n + 1` belongs to exactly one parent
at level `n`.
MapLibre receives these weighted nodes and applies its native Supercluster
implementation for pixel-perfect collision handling. When a server node is
reached, `expansion_zoom/5` identifies the first map zoom where that node has
more than one child cell.
"""
@world_half_meters 20_037_508.342_789_244
@world_width_meters @world_half_meters * 2.0
@cluster_level_offset 2
@minimum_level 0
@maximum_map_zoom 22
@maximum_level @maximum_map_zoom + @cluster_level_offset
@type kind :: :request | :activity
def level_for_zoom(zoom) when is_number(zoom) do
zoom
|> floor()
|> Kernel.+(@cluster_level_offset)
|> clamp(@minimum_level, @maximum_level)
end
def cell_size_meters(level) when is_integer(level) and level in 0..@maximum_level do
@world_width_meters / :math.pow(2.0, level)
end
def world_half_meters, do: @world_half_meters
def cluster_id(kind, level, cell_x, cell_y)
when kind in [:request, :activity] and is_integer(level) and is_integer(cell_x) and
is_integer(cell_y) do
"#{kind}:#{level}:#{cell_x}:#{cell_y}"
end
def parent_id(_kind, 0, _cell_x, _cell_y), do: nil
def parent_id(kind, level, cell_x, cell_y)
when kind in [:request, :activity] and is_integer(level) and level > 0 and
is_integer(cell_x) and is_integer(cell_y) do
cluster_id(kind, level - 1, div(cell_x, 2), div(cell_y, 2))
end
@doc """
Returns the first map zoom at which a fixed hierarchy cell separates.
The bounds must be expressed in EPSG:3857 meters and describe the actual
member points, not the cell envelope. For coincident points the result is the
maximum supported map zoom.
"""
def expansion_zoom(level, west, south, east, north)
when is_integer(level) and is_number(west) and is_number(south) and is_number(east) and
is_number(north) do
west = clamp_coordinate(west)
east = clamp_coordinate(east)
south = clamp_coordinate(south)
north = clamp_coordinate(north)
candidate_level =
if level >= @maximum_level do
@maximum_level
else
Enum.find((level + 1)..@maximum_level, @maximum_level, fn candidate ->
cell_index(west, candidate) != cell_index(east, candidate) or
cell_index(south, candidate) != cell_index(north, candidate)
end)
end
candidate_level
|> Kernel.-(@cluster_level_offset)
|> clamp(0, @maximum_map_zoom)
end
defp cell_index(coordinate, level) do
coordinate
|> Kernel.+(@world_half_meters)
|> Kernel./(cell_size_meters(level))
|> floor()
end
defp clamp_coordinate(value) do
value
|> Kernel.*(1.0)
|> max(-@world_half_meters)
|> min(@world_half_meters - 1.0e-7)
end
defp clamp(value, minimum, maximum), do: value |> max(minimum) |> min(maximum)
end

View File

@ -2,18 +2,18 @@ defmodule WhoNeedHelp.Help.DiscoveryViewport do
@moduledoc """
Validated map bounds used by request discovery.
The viewport carries its rendered pixel size so server-side clustering can
match the amount of detail the browser can actually display. Longitudes
crossing the antimeridian are represented as two PostGIS envelopes.
The viewport carries its rendered pixel size for subscription capacity and
input validation. Discovery clustering itself uses a stable zoom-indexed
hierarchy, so a cluster does not change identity when the canvas is resized.
Longitudes crossing the antimeridian are represented as two PostGIS
envelopes.
"""
@max_mercator_latitude 85.051_128_78
@maximum_canvas_dimension 4_096
@minimum_canvas_dimension 160
@cluster_cell_pixels 64.0
@map_tile_pixels 256
@maximum_subscription_zoom 12
@earth_radius_meters 6_378_137.0
@enforce_keys [:west, :south, :east, :north, :zoom, :width, :height]
defstruct [:west, :south, :east, :north, :zoom, :width, :height]
@ -106,22 +106,6 @@ defmodule WhoNeedHelp.Help.DiscoveryViewport do
|> Enum.uniq()
end
def cluster_cell_meters(%__MODULE__{} = viewport) do
horizontal_cells = max(viewport.width / @cluster_cell_pixels, 1.0)
vertical_cells = max(viewport.height / @cluster_cell_pixels, 1.0)
width_meters =
viewport
|> envelopes()
|> Enum.map(fn {west, _south, east, _north} -> mercator_x(east) - mercator_x(west) end)
|> Enum.sum()
height_meters = mercator_y(viewport.north) - mercator_y(viewport.south)
max(width_meters / horizontal_cells, height_meters / vertical_cells)
|> max(0.01)
end
def query_params(%__MODULE__{} = viewport) do
%{
"west" => format_number(viewport.west),
@ -310,14 +294,6 @@ defmodule WhoNeedHelp.Help.DiscoveryViewport do
defp latitude?(value),
do: value >= -@max_mercator_latitude and value <= @max_mercator_latitude
defp mercator_x(longitude), do: @earth_radius_meters * degrees_to_radians(longitude)
defp mercator_y(latitude) do
latitude = min(max(latitude, -@max_mercator_latitude), @max_mercator_latitude)
radians = degrees_to_radians(latitude)
@earth_radius_meters * :math.log(:math.tan(:math.pi() / 4 + radians / 2))
end
defp degrees_to_radians(value), do: value * :math.pi() / 180.0
defp clamp(value, minimum, maximum), do: min(max(value, minimum), maximum)

View File

@ -29,6 +29,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
|> assign(:activities_cursor, nil)
|> assign(:map_items, Jason.encode!([]))
|> assign(:map_result_count, 0)
|> assign(:map_total_count, 0)
|> assign(:discovery_loading, true)
|> assign(:refresh_timer, nil)
|> assign(:my_activities, [])
@ -266,6 +267,15 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
activities = activities_page.entries
map_total_count =
if discovery_mode == "nearby" and view_mode != "list" do
DiscoveryQueryCache.fetch(map_total_cache_key(scope, filters), fn ->
Activities.count_map_discovery_items(scope, filters)
end)
else
0
end
%{
discovery_loading: false,
activities: activities,
@ -273,7 +283,8 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
my_activities: my_activities_page.entries,
my_activities_cursor: my_activities_page.next_cursor,
map_items: Jason.encode!(map_items),
map_result_count: Enum.reduce(map_items, 0, &(map_item_count(&1) + &2))
map_result_count: Enum.reduce(map_items, 0, &(map_item_count(&1) + &2)),
map_total_count: map_total_count
}
end
@ -338,6 +349,11 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
)
)
:ok =
DiscoveryQueryCache.invalidate(
map_total_cache_key(socket.assigns.current_scope, socket.assigns.filters)
)
socket
end
@ -345,6 +361,8 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
{:activities, scope.user.id, filters, discovery_mode, view_mode, viewport}
end
defp map_total_cache_key(scope, filters), do: {:activity_map_total, scope.user.id, filters}
defp empty_filters, do: %{"category_id" => "", "area" => ""}
defp normalize_filters(params) do
@ -513,6 +531,11 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
count: @map_result_count
)}
</strong>
<span class="ml-1 text-sm text-base-content/60">
{gettext("of %{count} matching activities with map locations",
count: @map_total_count
)}
</span>
<p class="text-sm text-base-content/60">
{gettext("Move the map, then search the newly visible area.")}
</p>
@ -649,6 +672,9 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do
data-item-path-prefix="/activities"
data-item-label={gettext("Open activity")}
data-cluster-label={gettext("%{count} activities", count: "%{count}")}
data-cluster-max-zoom-label={
gettext("Maximum zoom reached. Matching activities are listed beside the map.")
}
data-map-unavailable-label={
gettext("The map is unavailable in this browser. Activity details remain usable.")
}

View File

@ -41,6 +41,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
|> assign(:reputation, @empty_reputation)
|> assign(:map_items, Jason.encode!([]))
|> assign(:map_result_count, 0)
|> assign(:map_total_count, 0)
|> assign(:discovery_loading, true)
|> assign(:refresh_timer, nil)
|> assign(:page_title, gettext("Nearby help"))
@ -259,6 +260,15 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
%Pagination.Page{}
end
map_total_count =
if discovery_mode == "nearby" and view_mode != "list" do
DiscoveryQueryCache.fetch(map_total_cache_key(scope, filters), fn ->
Help.count_map_discovery_items(scope, filters)
end)
else
0
end
%{
discovery_loading: false,
requests: requests_page.entries,
@ -267,7 +277,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
my_requests_cursor: my_requests_page.next_cursor,
reputation: Trust.reputation(scope.user.id),
map_items: Jason.encode!(map_items),
map_result_count: Enum.reduce(map_items, 0, &(map_item_count(&1) + &2))
map_result_count: Enum.reduce(map_items, 0, &(map_item_count(&1) + &2)),
map_total_count: map_total_count
}
end
@ -321,6 +332,11 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
)
)
:ok =
DiscoveryQueryCache.invalidate(
map_total_cache_key(socket.assigns.current_scope, socket.assigns.filters)
)
socket
end
@ -328,6 +344,8 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
{:requests, scope.user.id, filters, discovery_mode, view_mode, viewport}
end
defp map_total_cache_key(scope, filters), do: {:request_map_total, scope.user.id, filters}
defp empty_filters, do: %{"category_id" => "", "urgency" => "", "area" => ""}
defp normalize_filters(params) do
@ -525,6 +543,11 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
count: @map_result_count
)}
</strong>
<span class="ml-1 text-sm text-base-content/60">
{gettext("of %{count} matching requests with map locations",
count: @map_total_count
)}
</span>
<p class="text-sm text-base-content/60">
{gettext("Move the map, then search the newly visible area.")}
</p>
@ -680,6 +703,9 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
data-map-retry-label={gettext("Try map again")}
data-item-label={gettext("Open request")}
data-cluster-label={gettext("%{count} requests", count: "%{count}")}
data-cluster-max-zoom-label={
gettext("Maximum zoom reached. Matching requests are listed beside the map.")
}
class="aid-map request-discovery-map"
/>
<button

View File

@ -179,7 +179,7 @@ msgid "Active"
msgstr ""
#: lib/who_need_help_web/components/layouts.ex:231
#: lib/who_need_help_web/live/activity_live/index.ex:33
#: lib/who_need_help_web/live/activity_live/index.ex:37
#, elixir-autogen, elixir-format
msgid "Activities"
msgstr ""
@ -215,7 +215,7 @@ msgstr ""
msgid "Activity restored."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:427
#: lib/who_need_help_web/live/activity_live/index.ex:491
#: lib/who_need_help_web/live/activity_live/new.ex:204
#, elixir-autogen, elixir-format
msgid "Activity type"
@ -232,12 +232,12 @@ msgstr ""
msgid "Administrator"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:429
#: lib/who_need_help_web/live/activity_live/index.ex:493
#, elixir-autogen, elixir-format
msgid "All activities"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:407
#: lib/who_need_help_web/live/request_live/index.ex:493
#, elixir-autogen, elixir-format
msgid "All categories"
msgstr ""
@ -252,7 +252,7 @@ msgstr ""
msgid "Another helper already accepted this request."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:422
#: lib/who_need_help_web/live/request_live/index.ex:508
#, elixir-autogen, elixir-format
msgid "Any urgency"
msgstr ""
@ -367,16 +367,16 @@ msgstr ""
msgid "Cancel request"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:358
#: lib/who_need_help_web/live/activity_live/index.ex:422
#: lib/who_need_help_web/live/activity_live/show.ex:344
#: lib/who_need_help_web/live/request_live/index.ex:327
#: lib/who_need_help_web/live/request_live/index.ex:328
#: lib/who_need_help_web/live/request_live/index.ex:413
#: lib/who_need_help_web/live/request_live/index.ex:414
#: lib/who_need_help_web/live/request_live/show.ex:956
#, elixir-autogen, elixir-format
msgid "Cancelled"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:405
#: lib/who_need_help_web/live/request_live/index.ex:491
#: lib/who_need_help_web/live/request_live/new.ex:302
#, elixir-autogen, elixir-format
msgid "Category"
@ -459,7 +459,7 @@ msgstr ""
msgid "Coffee and conversation"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:372
#: lib/who_need_help_web/live/activity_live/index.ex:436
#, elixir-autogen, elixir-format
msgid "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings."
msgstr ""
@ -474,19 +474,19 @@ msgstr ""
msgid "Community helpers"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:578
#: lib/who_need_help_web/live/activity_live/index.ex:647
#: lib/who_need_help_web/live/activity_live/show.ex:403
#: lib/who_need_help_web/live/activity_live/show.ex:639
#: lib/who_need_help_web/live/activity_live/show.ex:698
#: lib/who_need_help_web/live/request_live/index.ex:565
#: lib/who_need_help_web/live/request_live/index.ex:656
#, elixir-autogen, elixir-format
msgid "Community member"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:357
#: lib/who_need_help_web/live/activity_live/index.ex:421
#: lib/who_need_help_web/live/activity_live/show.ex:343
#: lib/who_need_help_web/live/request_live/index.ex:325
#: lib/who_need_help_web/live/request_live/index.ex:326
#: lib/who_need_help_web/live/request_live/index.ex:411
#: lib/who_need_help_web/live/request_live/index.ex:412
#: lib/who_need_help_web/live/request_live/show.ex:955
#: lib/who_need_help_web/live/request_live/show.ex:974
#, elixir-autogen, elixir-format
@ -537,13 +537,13 @@ msgstr ""
msgid "Confirming..."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:378
#: lib/who_need_help_web/live/activity_live/index.ex:442
#: lib/who_need_help_web/live/activity_live/new.ex:12
#, elixir-autogen, elixir-format
msgid "Create activity"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:650
#: lib/who_need_help_web/live/activity_live/index.ex:722
#: lib/who_need_help_web/live/activity_live/new.ex:180
#, elixir-autogen, elixir-format
msgid "Create an activity"
@ -554,7 +554,7 @@ msgstr ""
msgid "Create category"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:356
#: lib/who_need_help_web/live/request_live/index.ex:442
#, elixir-autogen, elixir-format
msgid "Create urgent request"
msgstr ""
@ -704,7 +704,7 @@ msgstr ""
msgid "Explain what is already arranged and what the volunteer needs to do."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:369
#: lib/who_need_help_web/live/activity_live/index.ex:433
#, elixir-autogen, elixir-format
msgid "Find people to join"
msgstr ""
@ -865,8 +865,8 @@ msgstr ""
msgid "In a browser this works only while the page remains open. The Android app shows a persistent notification and can continue after it is minimized. Raw coordinates are removed when sharing stops."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:323
#: lib/who_need_help_web/live/request_live/index.ex:324
#: lib/who_need_help_web/live/request_live/index.ex:409
#: lib/who_need_help_web/live/request_live/index.ex:410
#: lib/who_need_help_web/live/request_live/show.ex:954
#, elixir-autogen, elixir-format
msgid "In progress"
@ -989,8 +989,8 @@ msgstr ""
msgid "Match and coordinate"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:321
#: lib/who_need_help_web/live/request_live/index.ex:322
#: lib/who_need_help_web/live/request_live/index.ex:407
#: lib/who_need_help_web/live/request_live/index.ex:408
#: lib/who_need_help_web/live/request_live/show.ex:952
#, elixir-autogen, elixir-format
msgid "Matched"
@ -1016,7 +1016,7 @@ msgstr ""
msgid "Medicine requests"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:385
#: lib/who_need_help_web/live/activity_live/index.ex:449
#, elixir-autogen, elixir-format
msgid "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe."
msgstr ""
@ -1067,12 +1067,12 @@ msgstr ""
msgid "Moderator access is required."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:347
#: lib/who_need_help_web/live/request_live/index.ex:433
#, elixir-autogen, elixir-format
msgid "NEARBY MUTUAL AID"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:34
#: lib/who_need_help_web/live/request_live/index.ex:47
#, elixir-autogen, elixir-format
msgid "Nearby help"
msgstr ""
@ -1153,9 +1153,9 @@ msgid "Not for emergencies"
msgstr ""
#: lib/who_need_help_web/live/notification_live.ex:748
#: lib/who_need_help_web/live/request_live/index.ex:311
#: lib/who_need_help_web/live/request_live/index.ex:312
#: lib/who_need_help_web/live/request_live/index.ex:423
#: lib/who_need_help_web/live/request_live/index.ex:397
#: lib/who_need_help_web/live/request_live/index.ex:398
#: lib/who_need_help_web/live/request_live/index.ex:509
#: lib/who_need_help_web/live/request_live/new.ex:449
#: lib/who_need_help_web/live/request_live/show.ex:943
#: lib/who_need_help_web/live/request_live/show.ex:944
@ -1163,7 +1163,7 @@ msgstr ""
msgid "Now"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:367
#: lib/who_need_help_web/live/activity_live/index.ex:431
#, elixir-autogen, elixir-format
msgid "OPTIONAL SOCIAL PLANS"
msgstr ""
@ -1181,12 +1181,12 @@ msgstr ""
#: lib/who_need_help/notifications/text.ex:140
#: lib/who_need_help_web/controllers/content_removal_html.ex:21
#: lib/who_need_help_web/controllers/support_html.ex:16
#: lib/who_need_help_web/live/activity_live/index.ex:356
#: lib/who_need_help_web/live/activity_live/index.ex:420
#: lib/who_need_help_web/live/activity_live/show.ex:342
#: lib/who_need_help_web/live/moderation_live.ex:258
#: lib/who_need_help_web/live/report_live.ex:104
#: lib/who_need_help_web/live/request_live/index.ex:319
#: lib/who_need_help_web/live/request_live/index.ex:320
#: lib/who_need_help_web/live/request_live/index.ex:405
#: lib/who_need_help_web/live/request_live/index.ex:406
#: lib/who_need_help_web/live/request_live/show.ex:951
#: lib/who_need_help_web/live/support_operations_live.ex:237
#: lib/who_need_help_web/live/support_operations_live.ex:438
@ -1639,9 +1639,9 @@ msgid "Save profile"
msgstr ""
#: lib/who_need_help_web/live/notification_live.ex:750
#: lib/who_need_help_web/live/request_live/index.ex:315
#: lib/who_need_help_web/live/request_live/index.ex:316
#: lib/who_need_help_web/live/request_live/index.ex:425
#: lib/who_need_help_web/live/request_live/index.ex:401
#: lib/who_need_help_web/live/request_live/index.ex:402
#: lib/who_need_help_web/live/request_live/index.ex:511
#: lib/who_need_help_web/live/request_live/new.ex:451
#: lib/who_need_help_web/live/request_live/show.ex:947
#: lib/who_need_help_web/live/request_live/show.ex:948
@ -1904,7 +1904,7 @@ msgstr ""
msgid "This interaction is blocked."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:363
#: lib/who_need_help_web/live/request_live/index.ex:449
#: lib/who_need_help_web/live/request_live/show.ex:1022
#, elixir-autogen, elixir-format
msgid "This is not an emergency service. Contact local emergency services for immediate danger."
@ -1952,9 +1952,9 @@ msgid "To see sent emails, visit"
msgstr ""
#: lib/who_need_help_web/live/notification_live.ex:749
#: lib/who_need_help_web/live/request_live/index.ex:313
#: lib/who_need_help_web/live/request_live/index.ex:314
#: lib/who_need_help_web/live/request_live/index.ex:424
#: lib/who_need_help_web/live/request_live/index.ex:399
#: lib/who_need_help_web/live/request_live/index.ex:400
#: lib/who_need_help_web/live/request_live/index.ex:510
#: lib/who_need_help_web/live/request_live/new.ex:450
#: lib/who_need_help_web/live/request_live/show.ex:945
#: lib/who_need_help_web/live/request_live/show.ex:946
@ -2029,7 +2029,7 @@ msgid "Update email instructions"
msgstr ""
#: lib/who_need_help_web/live/notification_live.ex:743
#: lib/who_need_help_web/live/request_live/index.ex:420
#: lib/who_need_help_web/live/request_live/index.ex:506
#: lib/who_need_help_web/live/request_live/new.ex:447
#, elixir-autogen, elixir-format
msgid "Urgency"
@ -2174,7 +2174,7 @@ msgstr ""
msgid "Who Need Help was developed with the local Codex CLI authenticated through the builder's ChatGPT subscription. No OpenAI API key or usage-based API fallback is used."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:348
#: lib/who_need_help_web/live/request_live/index.ex:434
#, elixir-autogen, elixir-format
msgid "Who needs help?"
msgstr ""
@ -2292,7 +2292,7 @@ msgstr ""
msgid "Your review is revealed only after both participants submit."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:630
#: lib/who_need_help_web/live/request_live/index.ex:724
#, elixir-autogen, elixir-format
msgid "Your trust summary"
msgstr ""
@ -2313,17 +2313,17 @@ msgstr ""
msgid "before confirming."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:577
#: lib/who_need_help_web/live/activity_live/index.ex:646
#: lib/who_need_help_web/live/category_proposal_live.ex:252
#: lib/who_need_help_web/live/moderation_live.ex:372
#: lib/who_need_help_web/live/request_live/index.ex:564
#: lib/who_need_help_web/live/request_live/index.ex:655
#, elixir-autogen, elixir-format
msgid "by %{name}"
msgstr ""
#: lib/who_need_help_web/live/profile_live.ex:190
#: lib/who_need_help_web/live/public_profile_live.ex:133
#: lib/who_need_help_web/live/request_live/index.ex:634
#: lib/who_need_help_web/live/request_live/index.ex:728
#: lib/who_need_help_web/live/request_live/show.ex:1374
#, elixir-autogen, elixir-format
msgid "completed"
@ -2359,7 +2359,7 @@ msgstr ""
msgid "participants only"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:638
#: lib/who_need_help_web/live/request_live/index.ex:732
#: lib/who_need_help_web/live/request_live/show.ex:1378
#, elixir-autogen, elixir-format
msgid "people"
@ -2419,7 +2419,7 @@ msgstr ""
#: lib/who_need_help_web/live/activity_live/show.ex:719
#: lib/who_need_help_web/live/profile_live.ex:285
#: lib/who_need_help_web/live/public_profile_live.ex:98
#: lib/who_need_help_web/live/request_live/index.ex:642
#: lib/who_need_help_web/live/request_live/index.ex:736
#: lib/who_need_help_web/live/request_live/show.ex:1093
#: lib/who_need_help_web/live/request_live/show.ex:1476
#, elixir-autogen, elixir-format
@ -2452,8 +2452,8 @@ msgstr ""
msgid "Approved"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:329
#: lib/who_need_help_web/live/request_live/index.ex:330
#: lib/who_need_help_web/live/request_live/index.ex:415
#: lib/who_need_help_web/live/request_live/index.ex:416
#, elixir-autogen, elixir-format
msgid "Expired"
msgstr ""
@ -2506,8 +2506,8 @@ msgstr ""
msgid "Load earlier messages"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:589
#: lib/who_need_help_web/live/activity_live/index.ex:679
#: lib/who_need_help_web/live/activity_live/index.ex:658
#: lib/who_need_help_web/live/activity_live/index.ex:751
#: lib/who_need_help_web/live/category_proposal_live.ex:284
#: lib/who_need_help_web/live/leaderboard_live.ex:91
#: lib/who_need_help_web/live/moderation_live.ex:465
@ -2519,7 +2519,7 @@ msgstr ""
#: lib/who_need_help_web/live/profile_live.ex:363
#: lib/who_need_help_web/live/public_profile_live.ex:178
#: lib/who_need_help_web/live/report_live.ex:98
#: lib/who_need_help_web/live/request_live/index.ex:699
#: lib/who_need_help_web/live/request_live/index.ex:805
#: lib/who_need_help_web/live/support_operations_live.ex:306
#: lib/who_need_help_web/live/support_operations_live.ex:505
#, elixir-autogen, elixir-format
@ -2527,7 +2527,7 @@ msgid "Load more"
msgstr ""
#: lib/who_need_help_web/live/activity_live/show.ex:544
#: lib/who_need_help_web/live/request_live/index.ex:610
#: lib/who_need_help_web/live/request_live/index.ex:701
#: lib/who_need_help_web/live/request_live/show.ex:1275
#, elixir-autogen, elixir-format
msgid "The map is unavailable in this browser. Request details remain usable."
@ -4516,44 +4516,44 @@ msgstr ""
msgid "Exact point selected. Drag the red pin or click the map to move it."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:453
#: lib/who_need_help_web/live/request_live/index.ex:539
#, elixir-autogen, elixir-format
msgid "%{count} request in this map area"
msgid_plural "%{count} requests in this map area"
msgstr[0] ""
msgstr[1] ""
#: lib/who_need_help_web/live/activity_live/index.ex:438
#: lib/who_need_help_web/live/request_live/index.ex:430
#: lib/who_need_help_web/live/activity_live/index.ex:502
#: lib/who_need_help_web/live/request_live/index.ex:516
#, elixir-autogen, elixir-format
msgid "Area or city"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:525
#: lib/who_need_help_web/live/request_live/index.ex:515
#: lib/who_need_help_web/live/activity_live/index.ex:594
#: lib/who_need_help_web/live/request_live/index.ex:606
#, elixir-autogen, elixir-format
msgid "Choose an area on the map"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:350
#: lib/who_need_help_web/live/request_live/index.ex:436
#, elixir-autogen, elixir-format
msgid "Explore privacy-friendly areas and public exact meeting points chosen by requesters."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:484
#: lib/who_need_help_web/live/request_live/index.ex:474
#: lib/who_need_help_web/live/activity_live/index.ex:553
#: lib/who_need_help_web/live/request_live/index.ex:565
#, elixir-autogen, elixir-format
msgid "List"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:586
#: lib/who_need_help_web/live/request_live/index.ex:677
#, elixir-autogen, elixir-format
msgid "Load more in this area"
msgstr ""
#: lib/who_need_help_web/components/layouts/root.html.heex:5
#: lib/who_need_help_web/live/activity_live/index.ex:504
#: lib/who_need_help_web/live/request_live/index.ex:494
#: lib/who_need_help_web/live/activity_live/index.ex:573
#: lib/who_need_help_web/live/request_live/index.ex:585
#, elixir-autogen, elixir-format
msgid "Map"
msgstr ""
@ -4563,53 +4563,53 @@ msgstr ""
msgid "Map preview"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:537
#: lib/who_need_help_web/live/request_live/index.ex:527
#: lib/who_need_help_web/live/activity_live/index.ex:606
#: lib/who_need_help_web/live/request_live/index.ex:618
#, elixir-autogen, elixir-format
msgid "Move or zoom the map to search somewhere else."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:471
#: lib/who_need_help_web/live/request_live/index.ex:461
#: lib/who_need_help_web/live/activity_live/index.ex:540
#: lib/who_need_help_web/live/request_live/index.ex:552
#, elixir-autogen, elixir-format
msgid "Move the map, then search the newly visible area."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:525
#: lib/who_need_help_web/live/request_live/index.ex:616
#, elixir-autogen, elixir-format
msgid "No open requests in this area"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:517
#: lib/who_need_help_web/live/request_live/index.ex:608
#, elixir-autogen, elixir-format
msgid "Only requests from the selected map area will be loaded."
msgstr ""
#: lib/who_need_help_web/components/layouts/root.html.heex:8
#: lib/who_need_help_web/live/request_live/index.ex:613
#: lib/who_need_help_web/live/request_live/index.ex:704
#, elixir-autogen, elixir-format
msgid "Open request"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:439
#: lib/who_need_help_web/live/request_live/index.ex:431
#: lib/who_need_help_web/live/activity_live/index.ex:503
#: lib/who_need_help_web/live/request_live/index.ex:517
#, elixir-autogen, elixir-format
msgid "Optional text search"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:592
#: lib/who_need_help_web/live/request_live/index.ex:683
#, elixir-autogen, elixir-format
msgid "Requests without map coordinates appear only when their public area description matches the text search."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:475
#: lib/who_need_help_web/live/request_live/index.ex:465
#: lib/who_need_help_web/live/activity_live/index.ex:544
#: lib/who_need_help_web/live/request_live/index.ex:556
#, elixir-autogen, elixir-format
msgid "Results layout"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:621
#: lib/who_need_help_web/live/request_live/index.ex:625
#: lib/who_need_help_web/live/activity_live/index.ex:693
#: lib/who_need_help_web/live/request_live/index.ex:719
#, elixir-autogen, elixir-format
msgid "Search this area"
msgstr ""
@ -4619,21 +4619,21 @@ msgstr ""
msgid "Static preview"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:494
#: lib/who_need_help_web/live/request_live/index.ex:484
#: lib/who_need_help_web/live/activity_live/index.ex:563
#: lib/who_need_help_web/live/request_live/index.ex:575
#, elixir-autogen, elixir-format
msgid "Together"
msgstr ""
#: lib/who_need_help_web/components/layouts/root.html.heex:6
#: lib/who_need_help_web/controllers/page_html/home.html.heex:160
#: lib/who_need_help_web/live/activity_live/index.ex:609
#: lib/who_need_help_web/live/activity_live/index.ex:681
#: lib/who_need_help_web/live/activity_live/new.ex:355
#: lib/who_need_help_web/live/activity_live/new.ex:414
#: lib/who_need_help_web/live/activity_live/show.ex:546
#: lib/who_need_help_web/live/notification_live.ex:602
#: lib/who_need_help_web/live/notification_live.ex:621
#: lib/who_need_help_web/live/request_live/index.ex:612
#: lib/who_need_help_web/live/request_live/index.ex:703
#: lib/who_need_help_web/live/request_live/new.ex:485
#: lib/who_need_help_web/live/request_live/new.ex:563
#: lib/who_need_help_web/live/request_live/show.ex:1277
@ -5022,7 +5022,7 @@ msgid "Nearby alert deleted."
msgstr ""
#: lib/who_need_help_web/live/notification_live.ex:456
#: lib/who_need_help_web/live/request_live/index.ex:376
#: lib/who_need_help_web/live/request_live/index.ex:462
#, elixir-autogen, elixir-format
msgid "Nearby requests"
msgstr ""
@ -5362,7 +5362,7 @@ msgid "WebGL is unavailable in this browser, so the full map cannot start. Selec
msgstr ""
#: lib/who_need_help_web/components/layouts/root.html.heex:7
#: lib/who_need_help_web/live/activity_live/index.ex:610
#: lib/who_need_help_web/live/activity_live/index.ex:682
#: lib/who_need_help_web/live/activity_live/show.ex:547
#: lib/who_need_help_web/live/request_live/show.ex:1278
#, elixir-autogen, elixir-format
@ -5927,7 +5927,7 @@ msgstr ""
msgid "Delivery settings"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:569
#: lib/who_need_help_web/live/request_live/index.ex:660
#, elixir-autogen, elixir-format
msgid "Expires"
msgstr ""
@ -5947,29 +5947,29 @@ msgstr ""
msgid "Notification sections"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:605
#: lib/who_need_help_web/live/activity_live/index.ex:674
#, elixir-autogen, elixir-format
msgid "%{count} activities"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:463
#: lib/who_need_help_web/live/activity_live/index.ex:527
#, elixir-autogen, elixir-format
msgid "%{count} activity in this map area"
msgid_plural "%{count} activities in this map area"
msgstr[0] ""
msgstr[1] ""
#: lib/who_need_help_web/live/request_live/index.ex:614
#: lib/who_need_help_web/live/request_live/index.ex:705
#, elixir-autogen, elixir-format
msgid "%{count} requests"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:645
#: lib/who_need_help_web/live/activity_live/index.ex:717
#, elixir-autogen, elixir-format
msgid "Activities you create or ask to join will appear here. Leaving removes an activity from this list."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:393
#: lib/who_need_help_web/live/activity_live/index.ex:457
#, elixir-autogen, elixir-format
msgid "Activity views"
msgstr ""
@ -5984,12 +5984,12 @@ msgstr ""
msgid "Confirm the safety guidance before asking nearby people for help."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:659
#: lib/who_need_help_web/live/request_live/index.ex:753
#, elixir-autogen, elixir-format
msgid "Create request"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:673
#: lib/who_need_help_web/live/request_live/index.ex:776
#, elixir-autogen, elixir-format
msgid "Create your first request"
msgstr ""
@ -6004,43 +6004,43 @@ msgstr ""
msgid "Location"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:399
#: lib/who_need_help_web/live/activity_live/index.ex:463
#, elixir-autogen, elixir-format
msgid "My activities"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:377
#: lib/who_need_help_web/live/request_live/index.ex:653
#: lib/who_need_help_web/live/request_live/index.ex:463
#: lib/who_need_help_web/live/request_live/index.ex:747
#, elixir-autogen, elixir-format
msgid "My requests"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:398
#: lib/who_need_help_web/live/activity_live/index.ex:462
#, elixir-autogen, elixir-format
msgid "Nearby activities"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:643
#: lib/who_need_help_web/live/activity_live/index.ex:715
#, elixir-autogen, elixir-format
msgid "No activities yet"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:535
#: lib/who_need_help_web/live/activity_live/index.ex:604
#, elixir-autogen, elixir-format
msgid "No open activities in this area"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:668
#: lib/who_need_help_web/live/request_live/index.ex:771
#, elixir-autogen, elixir-format
msgid "No requests yet"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:527
#: lib/who_need_help_web/live/activity_live/index.ex:596
#, elixir-autogen, elixir-format
msgid "Only activities from the selected map area will be loaded."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:604
#: lib/who_need_help_web/live/activity_live/index.ex:673
#, elixir-autogen, elixir-format
msgid "Open activity"
msgstr ""
@ -6050,12 +6050,12 @@ msgstr ""
msgid "Request form steps"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:371
#: lib/who_need_help_web/live/request_live/index.ex:457
#, elixir-autogen, elixir-format
msgid "Request views"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:670
#: lib/who_need_help_web/live/request_live/index.ex:773
#, elixir-autogen, elixir-format
msgid "Requests you publish or accept will appear here."
msgstr ""
@ -6080,7 +6080,7 @@ msgstr ""
msgid "Share only as much location detail as helpers need."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:607
#: lib/who_need_help_web/live/activity_live/index.ex:679
#, elixir-autogen, elixir-format
msgid "The map is unavailable in this browser. Activity details remain usable."
msgstr ""
@ -6090,7 +6090,7 @@ msgstr ""
msgid "Timing"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:655
#: lib/who_need_help_web/live/request_live/index.ex:749
#, elixir-autogen, elixir-format
msgid "Track requests you created and requests you accepted as a helper."
msgstr ""
@ -6751,11 +6751,32 @@ msgstr ""
msgid "Choose the meeting point on the map or use your current location."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:458
#: lib/who_need_help_web/live/activity_live/index.ex:517
#: lib/who_need_help_web/live/activity_live/index.ex:635
#: lib/who_need_help_web/live/request_live/index.ex:448
#: lib/who_need_help_web/live/request_live/index.ex:507
#: lib/who_need_help_web/live/activity_live/index.ex:522
#: lib/who_need_help_web/live/activity_live/index.ex:586
#: lib/who_need_help_web/live/activity_live/index.ex:707
#: lib/who_need_help_web/live/request_live/index.ex:534
#: lib/who_need_help_web/live/request_live/index.ex:598
#: lib/who_need_help_web/live/request_live/index.ex:763
#, elixir-autogen, elixir-format
msgid "Loading results…"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:676
#, elixir-autogen, elixir-format
msgid "Maximum zoom reached. Matching activities are listed beside the map."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:707
#, elixir-autogen, elixir-format
msgid "Maximum zoom reached. Matching requests are listed beside the map."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:535
#, elixir-autogen, elixir-format
msgid "of %{count} matching activities with map locations"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:547
#, elixir-autogen, elixir-format
msgid "of %{count} matching requests with map locations"
msgstr ""

View File

@ -179,7 +179,7 @@ msgid "Active"
msgstr "Active"
#: lib/who_need_help_web/components/layouts.ex:231
#: lib/who_need_help_web/live/activity_live/index.ex:33
#: lib/who_need_help_web/live/activity_live/index.ex:37
#, elixir-autogen, elixir-format
msgid "Activities"
msgstr "Activities"
@ -215,7 +215,7 @@ msgstr "Activity published. You approve every participant."
msgid "Activity restored."
msgstr "Activity restored."
#: lib/who_need_help_web/live/activity_live/index.ex:427
#: lib/who_need_help_web/live/activity_live/index.ex:491
#: lib/who_need_help_web/live/activity_live/new.ex:204
#, elixir-autogen, elixir-format
msgid "Activity type"
@ -232,12 +232,12 @@ msgstr "Add unverified link"
msgid "Administrator"
msgstr "Administrator"
#: lib/who_need_help_web/live/activity_live/index.ex:429
#: lib/who_need_help_web/live/activity_live/index.ex:493
#, elixir-autogen, elixir-format
msgid "All activities"
msgstr "All activities"
#: lib/who_need_help_web/live/request_live/index.ex:407
#: lib/who_need_help_web/live/request_live/index.ex:493
#, elixir-autogen, elixir-format
msgid "All categories"
msgstr "All categories"
@ -252,7 +252,7 @@ msgstr "Already registered?"
msgid "Another helper already accepted this request."
msgstr "Another helper already accepted this request."
#: lib/who_need_help_web/live/request_live/index.ex:422
#: lib/who_need_help_web/live/request_live/index.ex:508
#, elixir-autogen, elixir-format
msgid "Any urgency"
msgstr "Any urgency"
@ -367,16 +367,16 @@ msgstr "Cancel activity"
msgid "Cancel request"
msgstr "Cancel request"
#: lib/who_need_help_web/live/activity_live/index.ex:358
#: lib/who_need_help_web/live/activity_live/index.ex:422
#: lib/who_need_help_web/live/activity_live/show.ex:344
#: lib/who_need_help_web/live/request_live/index.ex:327
#: lib/who_need_help_web/live/request_live/index.ex:328
#: lib/who_need_help_web/live/request_live/index.ex:413
#: lib/who_need_help_web/live/request_live/index.ex:414
#: lib/who_need_help_web/live/request_live/show.ex:956
#, elixir-autogen, elixir-format
msgid "Cancelled"
msgstr "Cancelled"
#: lib/who_need_help_web/live/request_live/index.ex:405
#: lib/who_need_help_web/live/request_live/index.ex:491
#: lib/who_need_help_web/live/request_live/new.ex:302
#, elixir-autogen, elixir-format
msgid "Category"
@ -459,7 +459,7 @@ msgstr "Close"
msgid "Coffee and conversation"
msgstr "Coffee and conversation"
#: lib/who_need_help_web/live/activity_live/index.ex:372
#: lib/who_need_help_web/live/activity_live/index.ex:436
#, elixir-autogen, elixir-format
msgid "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings."
msgstr "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings."
@ -474,19 +474,19 @@ msgstr "Comment (optional)"
msgid "Community helpers"
msgstr "Community helpers"
#: lib/who_need_help_web/live/activity_live/index.ex:578
#: lib/who_need_help_web/live/activity_live/index.ex:647
#: lib/who_need_help_web/live/activity_live/show.ex:403
#: lib/who_need_help_web/live/activity_live/show.ex:639
#: lib/who_need_help_web/live/activity_live/show.ex:698
#: lib/who_need_help_web/live/request_live/index.ex:565
#: lib/who_need_help_web/live/request_live/index.ex:656
#, elixir-autogen, elixir-format
msgid "Community member"
msgstr "Community member"
#: lib/who_need_help_web/live/activity_live/index.ex:357
#: lib/who_need_help_web/live/activity_live/index.ex:421
#: lib/who_need_help_web/live/activity_live/show.ex:343
#: lib/who_need_help_web/live/request_live/index.ex:325
#: lib/who_need_help_web/live/request_live/index.ex:326
#: lib/who_need_help_web/live/request_live/index.ex:411
#: lib/who_need_help_web/live/request_live/index.ex:412
#: lib/who_need_help_web/live/request_live/show.ex:955
#: lib/who_need_help_web/live/request_live/show.ex:974
#, elixir-autogen, elixir-format
@ -537,13 +537,13 @@ msgstr "Confirmation instructions"
msgid "Confirming..."
msgstr "Confirming..."
#: lib/who_need_help_web/live/activity_live/index.ex:378
#: lib/who_need_help_web/live/activity_live/index.ex:442
#: lib/who_need_help_web/live/activity_live/new.ex:12
#, elixir-autogen, elixir-format
msgid "Create activity"
msgstr "Create activity"
#: lib/who_need_help_web/live/activity_live/index.ex:650
#: lib/who_need_help_web/live/activity_live/index.ex:722
#: lib/who_need_help_web/live/activity_live/new.ex:180
#, elixir-autogen, elixir-format
msgid "Create an activity"
@ -554,7 +554,7 @@ msgstr "Create an activity"
msgid "Create category"
msgstr "Create category"
#: lib/who_need_help_web/live/request_live/index.ex:356
#: lib/who_need_help_web/live/request_live/index.ex:442
#, elixir-autogen, elixir-format
msgid "Create urgent request"
msgstr "Create urgent request"
@ -704,7 +704,7 @@ msgstr "Expires:"
msgid "Explain what is already arranged and what the volunteer needs to do."
msgstr "Explain what is already arranged and what the volunteer needs to do."
#: lib/who_need_help_web/live/activity_live/index.ex:369
#: lib/who_need_help_web/live/activity_live/index.ex:433
#, elixir-autogen, elixir-format
msgid "Find people to join"
msgstr "Find people to join"
@ -865,8 +865,8 @@ msgstr "Impersonation"
msgid "In a browser this works only while the page remains open. The Android app shows a persistent notification and can continue after it is minimized. Raw coordinates are removed when sharing stops."
msgstr "In a browser this works only while the page remains open. The Android app shows a persistent notification and can continue after it is minimized. Raw coordinates are removed when sharing stops."
#: lib/who_need_help_web/live/request_live/index.ex:323
#: lib/who_need_help_web/live/request_live/index.ex:324
#: lib/who_need_help_web/live/request_live/index.ex:409
#: lib/who_need_help_web/live/request_live/index.ex:410
#: lib/who_need_help_web/live/request_live/show.ex:954
#, elixir-autogen, elixir-format
msgid "In progress"
@ -989,8 +989,8 @@ msgstr "Mark completed"
msgid "Match and coordinate"
msgstr "Match and coordinate"
#: lib/who_need_help_web/live/request_live/index.ex:321
#: lib/who_need_help_web/live/request_live/index.ex:322
#: lib/who_need_help_web/live/request_live/index.ex:407
#: lib/who_need_help_web/live/request_live/index.ex:408
#: lib/who_need_help_web/live/request_live/show.ex:952
#, elixir-autogen, elixir-format
msgid "Matched"
@ -1016,7 +1016,7 @@ msgstr "Medicine pickup remains the first priority. Roadside categories use the
msgid "Medicine requests"
msgstr "Medicine requests"
#: lib/who_need_help_web/live/activity_live/index.ex:385
#: lib/who_need_help_web/live/activity_live/index.ex:449
#, elixir-autogen, elixir-format
msgid "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe."
msgstr "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe."
@ -1067,12 +1067,12 @@ msgstr "Moderator"
msgid "Moderator access is required."
msgstr "Moderator access is required."
#: lib/who_need_help_web/live/request_live/index.ex:347
#: lib/who_need_help_web/live/request_live/index.ex:433
#, elixir-autogen, elixir-format
msgid "NEARBY MUTUAL AID"
msgstr "NEARBY MUTUAL AID"
#: lib/who_need_help_web/live/request_live/index.ex:34
#: lib/who_need_help_web/live/request_live/index.ex:47
#, elixir-autogen, elixir-format
msgid "Nearby help"
msgstr "Nearby help"
@ -1153,9 +1153,9 @@ msgid "Not for emergencies"
msgstr "Not for emergencies"
#: lib/who_need_help_web/live/notification_live.ex:748
#: lib/who_need_help_web/live/request_live/index.ex:311
#: lib/who_need_help_web/live/request_live/index.ex:312
#: lib/who_need_help_web/live/request_live/index.ex:423
#: lib/who_need_help_web/live/request_live/index.ex:397
#: lib/who_need_help_web/live/request_live/index.ex:398
#: lib/who_need_help_web/live/request_live/index.ex:509
#: lib/who_need_help_web/live/request_live/new.ex:449
#: lib/who_need_help_web/live/request_live/show.ex:943
#: lib/who_need_help_web/live/request_live/show.ex:944
@ -1163,7 +1163,7 @@ msgstr "Not for emergencies"
msgid "Now"
msgstr "Now"
#: lib/who_need_help_web/live/activity_live/index.ex:367
#: lib/who_need_help_web/live/activity_live/index.ex:431
#, elixir-autogen, elixir-format
msgid "OPTIONAL SOCIAL PLANS"
msgstr "OPTIONAL SOCIAL PLANS"
@ -1181,12 +1181,12 @@ msgstr "Only the organizer and approved participants can read or send these mess
#: lib/who_need_help/notifications/text.ex:140
#: lib/who_need_help_web/controllers/content_removal_html.ex:21
#: lib/who_need_help_web/controllers/support_html.ex:16
#: lib/who_need_help_web/live/activity_live/index.ex:356
#: lib/who_need_help_web/live/activity_live/index.ex:420
#: lib/who_need_help_web/live/activity_live/show.ex:342
#: lib/who_need_help_web/live/moderation_live.ex:258
#: lib/who_need_help_web/live/report_live.ex:104
#: lib/who_need_help_web/live/request_live/index.ex:319
#: lib/who_need_help_web/live/request_live/index.ex:320
#: lib/who_need_help_web/live/request_live/index.ex:405
#: lib/who_need_help_web/live/request_live/index.ex:406
#: lib/who_need_help_web/live/request_live/show.ex:951
#: lib/who_need_help_web/live/support_operations_live.ex:237
#: lib/who_need_help_web/live/support_operations_live.ex:438
@ -1639,9 +1639,9 @@ msgid "Save profile"
msgstr "Save profile"
#: lib/who_need_help_web/live/notification_live.ex:750
#: lib/who_need_help_web/live/request_live/index.ex:315
#: lib/who_need_help_web/live/request_live/index.ex:316
#: lib/who_need_help_web/live/request_live/index.ex:425
#: lib/who_need_help_web/live/request_live/index.ex:401
#: lib/who_need_help_web/live/request_live/index.ex:402
#: lib/who_need_help_web/live/request_live/index.ex:511
#: lib/who_need_help_web/live/request_live/new.ex:451
#: lib/who_need_help_web/live/request_live/show.ex:947
#: lib/who_need_help_web/live/request_live/show.ex:948
@ -1904,7 +1904,7 @@ msgstr "This activity state has already changed."
msgid "This interaction is blocked."
msgstr "This interaction is blocked."
#: lib/who_need_help_web/live/request_live/index.ex:363
#: lib/who_need_help_web/live/request_live/index.ex:449
#: lib/who_need_help_web/live/request_live/show.ex:1022
#, elixir-autogen, elixir-format
msgid "This is not an emergency service. Contact local emergency services for immediate danger."
@ -1952,9 +1952,9 @@ msgid "To see sent emails, visit"
msgstr "To see sent emails, visit"
#: lib/who_need_help_web/live/notification_live.ex:749
#: lib/who_need_help_web/live/request_live/index.ex:313
#: lib/who_need_help_web/live/request_live/index.ex:314
#: lib/who_need_help_web/live/request_live/index.ex:424
#: lib/who_need_help_web/live/request_live/index.ex:399
#: lib/who_need_help_web/live/request_live/index.ex:400
#: lib/who_need_help_web/live/request_live/index.ex:510
#: lib/who_need_help_web/live/request_live/new.ex:450
#: lib/who_need_help_web/live/request_live/show.ex:945
#: lib/who_need_help_web/live/request_live/show.ex:946
@ -2029,7 +2029,7 @@ msgid "Update email instructions"
msgstr "Update email instructions"
#: lib/who_need_help_web/live/notification_live.ex:743
#: lib/who_need_help_web/live/request_live/index.ex:420
#: lib/who_need_help_web/live/request_live/index.ex:506
#: lib/who_need_help_web/live/request_live/new.ex:447
#, elixir-autogen, elixir-format
msgid "Urgency"
@ -2174,7 +2174,7 @@ msgstr "Who Need Help home"
msgid "Who Need Help was developed with the local Codex CLI authenticated through the builder's ChatGPT subscription. No OpenAI API key or usage-based API fallback is used."
msgstr "Who Need Help was developed with the local Codex CLI authenticated through the builder's ChatGPT subscription. No OpenAI API key or usage-based API fallback is used."
#: lib/who_need_help_web/live/request_live/index.ex:348
#: lib/who_need_help_web/live/request_live/index.ex:434
#, elixir-autogen, elixir-format
msgid "Who needs help?"
msgstr "Who needs help?"
@ -2292,7 +2292,7 @@ msgstr "Your requests"
msgid "Your review is revealed only after both participants submit."
msgstr "Your review is revealed only after both participants submit."
#: lib/who_need_help_web/live/request_live/index.ex:630
#: lib/who_need_help_web/live/request_live/index.ex:724
#, elixir-autogen, elixir-format
msgid "Your trust summary"
msgstr "Your trust summary"
@ -2313,17 +2313,17 @@ msgstr "activity message"
msgid "before confirming."
msgstr "before confirming."
#: lib/who_need_help_web/live/activity_live/index.ex:577
#: lib/who_need_help_web/live/activity_live/index.ex:646
#: lib/who_need_help_web/live/category_proposal_live.ex:252
#: lib/who_need_help_web/live/moderation_live.ex:372
#: lib/who_need_help_web/live/request_live/index.ex:564
#: lib/who_need_help_web/live/request_live/index.ex:655
#, elixir-autogen, elixir-format
msgid "by %{name}"
msgstr "by %{name}"
#: lib/who_need_help_web/live/profile_live.ex:190
#: lib/who_need_help_web/live/public_profile_live.ex:133
#: lib/who_need_help_web/live/request_live/index.ex:634
#: lib/who_need_help_web/live/request_live/index.ex:728
#: lib/who_need_help_web/live/request_live/show.ex:1374
#, elixir-autogen, elixir-format
msgid "completed"
@ -2359,7 +2359,7 @@ msgstr "organizer"
msgid "participants only"
msgstr "participants only"
#: lib/who_need_help_web/live/request_live/index.ex:638
#: lib/who_need_help_web/live/request_live/index.ex:732
#: lib/who_need_help_web/live/request_live/show.ex:1378
#, elixir-autogen, elixir-format
msgid "people"
@ -2419,7 +2419,7 @@ msgstr "urgent"
#: lib/who_need_help_web/live/activity_live/show.ex:719
#: lib/who_need_help_web/live/profile_live.ex:285
#: lib/who_need_help_web/live/public_profile_live.ex:98
#: lib/who_need_help_web/live/request_live/index.ex:642
#: lib/who_need_help_web/live/request_live/index.ex:736
#: lib/who_need_help_web/live/request_live/show.ex:1093
#: lib/who_need_help_web/live/request_live/show.ex:1476
#, elixir-autogen, elixir-format
@ -2452,8 +2452,8 @@ msgstr "← Requests"
msgid "Approved"
msgstr "Approved"
#: lib/who_need_help_web/live/request_live/index.ex:329
#: lib/who_need_help_web/live/request_live/index.ex:330
#: lib/who_need_help_web/live/request_live/index.ex:415
#: lib/who_need_help_web/live/request_live/index.ex:416
#, elixir-autogen, elixir-format
msgid "Expired"
msgstr "Expired"
@ -2506,8 +2506,8 @@ msgstr "Unusual action rate"
msgid "Load earlier messages"
msgstr "Load earlier messages"
#: lib/who_need_help_web/live/activity_live/index.ex:589
#: lib/who_need_help_web/live/activity_live/index.ex:679
#: lib/who_need_help_web/live/activity_live/index.ex:658
#: lib/who_need_help_web/live/activity_live/index.ex:751
#: lib/who_need_help_web/live/category_proposal_live.ex:284
#: lib/who_need_help_web/live/leaderboard_live.ex:91
#: lib/who_need_help_web/live/moderation_live.ex:465
@ -2519,7 +2519,7 @@ msgstr "Load earlier messages"
#: lib/who_need_help_web/live/profile_live.ex:363
#: lib/who_need_help_web/live/public_profile_live.ex:178
#: lib/who_need_help_web/live/report_live.ex:98
#: lib/who_need_help_web/live/request_live/index.ex:699
#: lib/who_need_help_web/live/request_live/index.ex:805
#: lib/who_need_help_web/live/support_operations_live.ex:306
#: lib/who_need_help_web/live/support_operations_live.ex:505
#, elixir-autogen, elixir-format
@ -2527,7 +2527,7 @@ msgid "Load more"
msgstr "Load more"
#: lib/who_need_help_web/live/activity_live/show.ex:544
#: lib/who_need_help_web/live/request_live/index.ex:610
#: lib/who_need_help_web/live/request_live/index.ex:701
#: lib/who_need_help_web/live/request_live/show.ex:1275
#, elixir-autogen, elixir-format
msgid "The map is unavailable in this browser. Request details remain usable."
@ -4516,44 +4516,44 @@ msgstr ""
msgid "Exact point selected. Drag the red pin or click the map to move it."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:453
#: lib/who_need_help_web/live/request_live/index.ex:539
#, elixir-autogen, elixir-format
msgid "%{count} request in this map area"
msgid_plural "%{count} requests in this map area"
msgstr[0] ""
msgstr[1] ""
#: lib/who_need_help_web/live/activity_live/index.ex:438
#: lib/who_need_help_web/live/request_live/index.ex:430
#: lib/who_need_help_web/live/activity_live/index.ex:502
#: lib/who_need_help_web/live/request_live/index.ex:516
#, elixir-autogen, elixir-format
msgid "Area or city"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:525
#: lib/who_need_help_web/live/request_live/index.ex:515
#: lib/who_need_help_web/live/activity_live/index.ex:594
#: lib/who_need_help_web/live/request_live/index.ex:606
#, elixir-autogen, elixir-format
msgid "Choose an area on the map"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:350
#: lib/who_need_help_web/live/request_live/index.ex:436
#, elixir-autogen, elixir-format
msgid "Explore privacy-friendly areas and public exact meeting points chosen by requesters."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:484
#: lib/who_need_help_web/live/request_live/index.ex:474
#: lib/who_need_help_web/live/activity_live/index.ex:553
#: lib/who_need_help_web/live/request_live/index.ex:565
#, elixir-autogen, elixir-format
msgid "List"
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:586
#: lib/who_need_help_web/live/request_live/index.ex:677
#, elixir-autogen, elixir-format
msgid "Load more in this area"
msgstr "Load more in this area"
#: lib/who_need_help_web/components/layouts/root.html.heex:5
#: lib/who_need_help_web/live/activity_live/index.ex:504
#: lib/who_need_help_web/live/request_live/index.ex:494
#: lib/who_need_help_web/live/activity_live/index.ex:573
#: lib/who_need_help_web/live/request_live/index.ex:585
#, elixir-autogen, elixir-format
msgid "Map"
msgstr ""
@ -4563,53 +4563,53 @@ msgstr ""
msgid "Map preview"
msgstr "Map preview"
#: lib/who_need_help_web/live/activity_live/index.ex:537
#: lib/who_need_help_web/live/request_live/index.ex:527
#: lib/who_need_help_web/live/activity_live/index.ex:606
#: lib/who_need_help_web/live/request_live/index.ex:618
#, elixir-autogen, elixir-format
msgid "Move or zoom the map to search somewhere else."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:471
#: lib/who_need_help_web/live/request_live/index.ex:461
#: lib/who_need_help_web/live/activity_live/index.ex:540
#: lib/who_need_help_web/live/request_live/index.ex:552
#, elixir-autogen, elixir-format
msgid "Move the map, then search the newly visible area."
msgstr ""
#: lib/who_need_help_web/live/request_live/index.ex:525
#: lib/who_need_help_web/live/request_live/index.ex:616
#, elixir-autogen, elixir-format
msgid "No open requests in this area"
msgstr "No open requests in this area"
#: lib/who_need_help_web/live/request_live/index.ex:517
#: lib/who_need_help_web/live/request_live/index.ex:608
#, elixir-autogen, elixir-format
msgid "Only requests from the selected map area will be loaded."
msgstr ""
#: lib/who_need_help_web/components/layouts/root.html.heex:8
#: lib/who_need_help_web/live/request_live/index.ex:613
#: lib/who_need_help_web/live/request_live/index.ex:704
#, elixir-autogen, elixir-format
msgid "Open request"
msgstr "Open request"
#: lib/who_need_help_web/live/activity_live/index.ex:439
#: lib/who_need_help_web/live/request_live/index.ex:431
#: lib/who_need_help_web/live/activity_live/index.ex:503
#: lib/who_need_help_web/live/request_live/index.ex:517
#, elixir-autogen, elixir-format
msgid "Optional text search"
msgstr "Optional text search"
#: lib/who_need_help_web/live/request_live/index.ex:592
#: lib/who_need_help_web/live/request_live/index.ex:683
#, elixir-autogen, elixir-format
msgid "Requests without map coordinates appear only when their public area description matches the text search."
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:475
#: lib/who_need_help_web/live/request_live/index.ex:465
#: lib/who_need_help_web/live/activity_live/index.ex:544
#: lib/who_need_help_web/live/request_live/index.ex:556
#, elixir-autogen, elixir-format
msgid "Results layout"
msgstr ""
#: lib/who_need_help_web/live/activity_live/index.ex:621
#: lib/who_need_help_web/live/request_live/index.ex:625
#: lib/who_need_help_web/live/activity_live/index.ex:693
#: lib/who_need_help_web/live/request_live/index.ex:719
#, elixir-autogen, elixir-format
msgid "Search this area"
msgstr ""
@ -4619,21 +4619,21 @@ msgstr ""
msgid "Static preview"
msgstr "Static preview"
#: lib/who_need_help_web/live/activity_live/index.ex:494
#: lib/who_need_help_web/live/request_live/index.ex:484
#: lib/who_need_help_web/live/activity_live/index.ex:563
#: lib/who_need_help_web/live/request_live/index.ex:575
#, elixir-autogen, elixir-format
msgid "Together"
msgstr ""
#: lib/who_need_help_web/components/layouts/root.html.heex:6
#: lib/who_need_help_web/controllers/page_html/home.html.heex:160
#: lib/who_need_help_web/live/activity_live/index.ex:609
#: lib/who_need_help_web/live/activity_live/index.ex:681
#: lib/who_need_help_web/live/activity_live/new.ex:355
#: lib/who_need_help_web/live/activity_live/new.ex:414
#: lib/who_need_help_web/live/activity_live/show.ex:546
#: lib/who_need_help_web/live/notification_live.ex:602
#: lib/who_need_help_web/live/notification_live.ex:621
#: lib/who_need_help_web/live/request_live/index.ex:612
#: lib/who_need_help_web/live/request_live/index.ex:703
#: lib/who_need_help_web/live/request_live/new.ex:485
#: lib/who_need_help_web/live/request_live/new.ex:563
#: lib/who_need_help_web/live/request_live/show.ex:1277
@ -5022,7 +5022,7 @@ msgid "Nearby alert deleted."
msgstr ""
#: lib/who_need_help_web/live/notification_live.ex:456
#: lib/who_need_help_web/live/request_live/index.ex:376
#: lib/who_need_help_web/live/request_live/index.ex:462
#, elixir-autogen, elixir-format
msgid "Nearby requests"
msgstr ""
@ -5362,7 +5362,7 @@ msgid "WebGL is unavailable in this browser, so the full map cannot start. Selec
msgstr "WebGL is unavailable in this browser, so the full map cannot start. Select an example below or retry the map."
#: lib/who_need_help_web/components/layouts/root.html.heex:7
#: lib/who_need_help_web/live/activity_live/index.ex:610
#: lib/who_need_help_web/live/activity_live/index.ex:682
#: lib/who_need_help_web/live/activity_live/show.ex:547
#: lib/who_need_help_web/live/request_live/show.ex:1278
#, elixir-autogen, elixir-format
@ -5927,7 +5927,7 @@ msgstr "Your review is now visible"
msgid "Delivery settings"
msgstr "Delivery settings"
#: lib/who_need_help_web/live/request_live/index.ex:569
#: lib/who_need_help_web/live/request_live/index.ex:660
#, elixir-autogen, elixir-format
msgid "Expires"
msgstr "Expires"
@ -5947,29 +5947,29 @@ msgstr "Nearby alerts"
msgid "Notification sections"
msgstr "Notification sections"
#: lib/who_need_help_web/live/activity_live/index.ex:605
#: lib/who_need_help_web/live/activity_live/index.ex:674
#, elixir-autogen, elixir-format
msgid "%{count} activities"
msgstr "%{count} activities"
#: lib/who_need_help_web/live/activity_live/index.ex:463
#: lib/who_need_help_web/live/activity_live/index.ex:527
#, elixir-autogen, elixir-format
msgid "%{count} activity in this map area"
msgid_plural "%{count} activities in this map area"
msgstr[0] "%{count} activity in this map area"
msgstr[1] "%{count} activities in this map area"
#: lib/who_need_help_web/live/request_live/index.ex:614
#: lib/who_need_help_web/live/request_live/index.ex:705
#, elixir-autogen, elixir-format
msgid "%{count} requests"
msgstr "%{count} requests"
#: lib/who_need_help_web/live/activity_live/index.ex:645
#: lib/who_need_help_web/live/activity_live/index.ex:717
#, elixir-autogen, elixir-format
msgid "Activities you create or ask to join will appear here. Leaving removes an activity from this list."
msgstr "Activities you create or ask to join will appear here. Leaving removes an activity from this list."
#: lib/who_need_help_web/live/activity_live/index.ex:393
#: lib/who_need_help_web/live/activity_live/index.ex:457
#, elixir-autogen, elixir-format
msgid "Activity views"
msgstr "Activity views"
@ -5984,12 +5984,12 @@ msgstr "Choose a category and describe the task clearly."
msgid "Confirm the safety guidance before asking nearby people for help."
msgstr "Confirm the safety guidance before asking nearby people for help."
#: lib/who_need_help_web/live/request_live/index.ex:659
#: lib/who_need_help_web/live/request_live/index.ex:753
#, elixir-autogen, elixir-format
msgid "Create request"
msgstr "Create request"
#: lib/who_need_help_web/live/request_live/index.ex:673
#: lib/who_need_help_web/live/request_live/index.ex:776
#, elixir-autogen, elixir-format
msgid "Create your first request"
msgstr "Create your first request"
@ -6004,43 +6004,43 @@ msgstr "Help details"
msgid "Location"
msgstr "Location"
#: lib/who_need_help_web/live/activity_live/index.ex:399
#: lib/who_need_help_web/live/activity_live/index.ex:463
#, elixir-autogen, elixir-format
msgid "My activities"
msgstr "My activities"
#: lib/who_need_help_web/live/request_live/index.ex:377
#: lib/who_need_help_web/live/request_live/index.ex:653
#: lib/who_need_help_web/live/request_live/index.ex:463
#: lib/who_need_help_web/live/request_live/index.ex:747
#, elixir-autogen, elixir-format
msgid "My requests"
msgstr "My requests"
#: lib/who_need_help_web/live/activity_live/index.ex:398
#: lib/who_need_help_web/live/activity_live/index.ex:462
#, elixir-autogen, elixir-format
msgid "Nearby activities"
msgstr "Nearby activities"
#: lib/who_need_help_web/live/activity_live/index.ex:643
#: lib/who_need_help_web/live/activity_live/index.ex:715
#, elixir-autogen, elixir-format
msgid "No activities yet"
msgstr "No activities yet"
#: lib/who_need_help_web/live/activity_live/index.ex:535
#: lib/who_need_help_web/live/activity_live/index.ex:604
#, elixir-autogen, elixir-format
msgid "No open activities in this area"
msgstr "No open activities in this area"
#: lib/who_need_help_web/live/request_live/index.ex:668
#: lib/who_need_help_web/live/request_live/index.ex:771
#, elixir-autogen, elixir-format
msgid "No requests yet"
msgstr "No requests yet"
#: lib/who_need_help_web/live/activity_live/index.ex:527
#: lib/who_need_help_web/live/activity_live/index.ex:596
#, elixir-autogen, elixir-format
msgid "Only activities from the selected map area will be loaded."
msgstr "Only activities from the selected map area will be loaded."
#: lib/who_need_help_web/live/activity_live/index.ex:604
#: lib/who_need_help_web/live/activity_live/index.ex:673
#, elixir-autogen, elixir-format
msgid "Open activity"
msgstr "Open activity"
@ -6050,12 +6050,12 @@ msgstr "Open activity"
msgid "Request form steps"
msgstr "Request form steps"
#: lib/who_need_help_web/live/request_live/index.ex:371
#: lib/who_need_help_web/live/request_live/index.ex:457
#, elixir-autogen, elixir-format
msgid "Request views"
msgstr "Request views"
#: lib/who_need_help_web/live/request_live/index.ex:670
#: lib/who_need_help_web/live/request_live/index.ex:773
#, elixir-autogen, elixir-format
msgid "Requests you publish or accept will appear here."
msgstr "Requests you publish or accept will appear here."
@ -6080,7 +6080,7 @@ msgstr "Set urgency and when this request should close."
msgid "Share only as much location detail as helpers need."
msgstr "Share only as much location detail as helpers need."
#: lib/who_need_help_web/live/activity_live/index.ex:607
#: lib/who_need_help_web/live/activity_live/index.ex:679
#, elixir-autogen, elixir-format
msgid "The map is unavailable in this browser. Activity details remain usable."
msgstr "The map is unavailable in this browser. Activity details remain usable."
@ -6090,7 +6090,7 @@ msgstr "The map is unavailable in this browser. Activity details remain usable."
msgid "Timing"
msgstr "Timing"
#: lib/who_need_help_web/live/request_live/index.ex:655
#: lib/who_need_help_web/live/request_live/index.ex:749
#, elixir-autogen, elixir-format
msgid "Track requests you created and requests you accepted as a helper."
msgstr "Track requests you created and requests you accepted as a helper."
@ -6751,11 +6751,32 @@ msgstr "What should people see before approval?"
msgid "Choose the meeting point on the map or use your current location."
msgstr "Choose the meeting point on the map or use your current location."
#: lib/who_need_help_web/live/activity_live/index.ex:458
#: lib/who_need_help_web/live/activity_live/index.ex:517
#: lib/who_need_help_web/live/activity_live/index.ex:635
#: lib/who_need_help_web/live/request_live/index.ex:448
#: lib/who_need_help_web/live/request_live/index.ex:507
#: lib/who_need_help_web/live/activity_live/index.ex:522
#: lib/who_need_help_web/live/activity_live/index.ex:586
#: lib/who_need_help_web/live/activity_live/index.ex:707
#: lib/who_need_help_web/live/request_live/index.ex:534
#: lib/who_need_help_web/live/request_live/index.ex:598
#: lib/who_need_help_web/live/request_live/index.ex:763
#, elixir-autogen, elixir-format
msgid "Loading results…"
msgstr "Loading results…"
#: lib/who_need_help_web/live/activity_live/index.ex:676
#, elixir-autogen, elixir-format
msgid "Maximum zoom reached. Matching activities are listed beside the map."
msgstr "Maximum zoom reached. Matching activities are listed beside the map."
#: lib/who_need_help_web/live/request_live/index.ex:707
#, elixir-autogen, elixir-format
msgid "Maximum zoom reached. Matching requests are listed beside the map."
msgstr "Maximum zoom reached. Matching requests are listed beside the map."
#: lib/who_need_help_web/live/activity_live/index.ex:535
#, elixir-autogen, elixir-format
msgid "of %{count} matching activities with map locations"
msgstr "of %{count} matching activities with map locations"
#: lib/who_need_help_web/live/request_live/index.ex:547
#, elixir-autogen, elixir-format
msgid "of %{count} matching requests with map locations"
msgstr "of %{count} matching requests with map locations"

View File

@ -184,7 +184,7 @@ msgid "Active"
msgstr "Активный"
#: lib/who_need_help_web/components/layouts.ex:231
#: lib/who_need_help_web/live/activity_live/index.ex:33
#: lib/who_need_help_web/live/activity_live/index.ex:37
#, elixir-autogen, elixir-format
msgid "Activities"
msgstr "Активности"
@ -226,7 +226,7 @@ msgstr "Активность опубликована. Вы одобряете
msgid "Activity restored."
msgstr "Активность восстановлена."
#: lib/who_need_help_web/live/activity_live/index.ex:427
#: lib/who_need_help_web/live/activity_live/index.ex:491
#: lib/who_need_help_web/live/activity_live/new.ex:204
#, elixir-autogen, elixir-format
msgid "Activity type"
@ -243,12 +243,12 @@ msgstr "Добавить неподтверждённую ссылку"
msgid "Administrator"
msgstr "Администратор"
#: lib/who_need_help_web/live/activity_live/index.ex:429
#: lib/who_need_help_web/live/activity_live/index.ex:493
#, elixir-autogen, elixir-format
msgid "All activities"
msgstr "Все активности"
#: lib/who_need_help_web/live/request_live/index.ex:407
#: lib/who_need_help_web/live/request_live/index.ex:493
#, elixir-autogen, elixir-format
msgid "All categories"
msgstr "Все категории"
@ -263,7 +263,7 @@ msgstr "Уже зарегистрированы?"
msgid "Another helper already accepted this request."
msgstr "Другой помощник уже принял эту заявку."
#: lib/who_need_help_web/live/request_live/index.ex:422
#: lib/who_need_help_web/live/request_live/index.ex:508
#, elixir-autogen, elixir-format
msgid "Any urgency"
msgstr "Любая срочность"
@ -383,16 +383,16 @@ msgstr "Отменить активность"
msgid "Cancel request"
msgstr "Отменить заявку"
#: lib/who_need_help_web/live/activity_live/index.ex:358
#: lib/who_need_help_web/live/activity_live/index.ex:422
#: lib/who_need_help_web/live/activity_live/show.ex:344
#: lib/who_need_help_web/live/request_live/index.ex:327
#: lib/who_need_help_web/live/request_live/index.ex:328
#: lib/who_need_help_web/live/request_live/index.ex:413
#: lib/who_need_help_web/live/request_live/index.ex:414
#: lib/who_need_help_web/live/request_live/show.ex:956
#, elixir-autogen, elixir-format
msgid "Cancelled"
msgstr "Отменено"
#: lib/who_need_help_web/live/request_live/index.ex:405
#: lib/who_need_help_web/live/request_live/index.ex:491
#: lib/who_need_help_web/live/request_live/new.ex:302
#, elixir-autogen, elixir-format
msgid "Category"
@ -476,7 +476,7 @@ msgstr "Закрыть"
msgid "Coffee and conversation"
msgstr "Кофе и разговоры"
#: lib/who_need_help_web/live/activity_live/index.ex:372
#: lib/who_need_help_web/live/activity_live/index.ex:436
#, elixir-autogen, elixir-format
msgid "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings."
msgstr ""
@ -493,19 +493,19 @@ msgstr "Комментарий (необязательно)"
msgid "Community helpers"
msgstr "Помощники сообщества"
#: lib/who_need_help_web/live/activity_live/index.ex:578
#: lib/who_need_help_web/live/activity_live/index.ex:647
#: lib/who_need_help_web/live/activity_live/show.ex:403
#: lib/who_need_help_web/live/activity_live/show.ex:639
#: lib/who_need_help_web/live/activity_live/show.ex:698
#: lib/who_need_help_web/live/request_live/index.ex:565
#: lib/who_need_help_web/live/request_live/index.ex:656
#, elixir-autogen, elixir-format
msgid "Community member"
msgstr "Член сообщества"
#: lib/who_need_help_web/live/activity_live/index.ex:357
#: lib/who_need_help_web/live/activity_live/index.ex:421
#: lib/who_need_help_web/live/activity_live/show.ex:343
#: lib/who_need_help_web/live/request_live/index.ex:325
#: lib/who_need_help_web/live/request_live/index.ex:326
#: lib/who_need_help_web/live/request_live/index.ex:411
#: lib/who_need_help_web/live/request_live/index.ex:412
#: lib/who_need_help_web/live/request_live/show.ex:955
#: lib/who_need_help_web/live/request_live/show.ex:974
#, elixir-autogen, elixir-format
@ -558,13 +558,13 @@ msgstr "Инструкции по подтверждению"
msgid "Confirming..."
msgstr "Подтверждение…"
#: lib/who_need_help_web/live/activity_live/index.ex:378
#: lib/who_need_help_web/live/activity_live/index.ex:442
#: lib/who_need_help_web/live/activity_live/new.ex:12
#, elixir-autogen, elixir-format
msgid "Create activity"
msgstr "Создать активность"
#: lib/who_need_help_web/live/activity_live/index.ex:650
#: lib/who_need_help_web/live/activity_live/index.ex:722
#: lib/who_need_help_web/live/activity_live/new.ex:180
#, elixir-autogen, elixir-format
msgid "Create an activity"
@ -575,7 +575,7 @@ msgstr "Создать активность"
msgid "Create category"
msgstr "Создать категорию"
#: lib/who_need_help_web/live/request_live/index.ex:356
#: lib/who_need_help_web/live/request_live/index.ex:442
#, elixir-autogen, elixir-format
msgid "Create urgent request"
msgstr "Создать срочную заявку"
@ -737,7 +737,7 @@ msgstr "Истекает:"
msgid "Explain what is already arranged and what the volunteer needs to do."
msgstr "Объясните, что уже сделано и что нужно сделать волонтеру."
#: lib/who_need_help_web/live/activity_live/index.ex:369
#: lib/who_need_help_web/live/activity_live/index.ex:433
#, elixir-autogen, elixir-format
msgid "Find people to join"
msgstr "Найти компанию"
@ -938,8 +938,8 @@ msgstr ""
"Android показывает постоянное уведомление и может продолжать передачу после "
"сворачивания. При остановке передачи исходные координаты удаляются."
#: lib/who_need_help_web/live/request_live/index.ex:323
#: lib/who_need_help_web/live/request_live/index.ex:324
#: lib/who_need_help_web/live/request_live/index.ex:409
#: lib/who_need_help_web/live/request_live/index.ex:410
#: lib/who_need_help_web/live/request_live/show.ex:954
#, elixir-autogen, elixir-format
msgid "In progress"
@ -1069,8 +1069,8 @@ msgstr "Отметить завершённой"
msgid "Match and coordinate"
msgstr "Найдите помощника и согласуйте детали"
#: lib/who_need_help_web/live/request_live/index.ex:321
#: lib/who_need_help_web/live/request_live/index.ex:322
#: lib/who_need_help_web/live/request_live/index.ex:407
#: lib/who_need_help_web/live/request_live/index.ex:408
#: lib/who_need_help_web/live/request_live/show.ex:952
#, elixir-autogen, elixir-format
msgid "Matched"
@ -1099,7 +1099,7 @@ msgstr ""
msgid "Medicine requests"
msgstr "Заявки на лекарства"
#: lib/who_need_help_web/live/activity_live/index.ex:385
#: lib/who_need_help_web/live/activity_live/index.ex:449
#, elixir-autogen, elixir-format
msgid "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe."
msgstr ""
@ -1153,12 +1153,12 @@ msgstr "Модератор"
msgid "Moderator access is required."
msgstr "Требуется доступ модератора."
#: lib/who_need_help_web/live/request_live/index.ex:347
#: lib/who_need_help_web/live/request_live/index.ex:433
#, elixir-autogen, elixir-format
msgid "NEARBY MUTUAL AID"
msgstr "ВЗАИМОПОМОЩЬ РЯДОМ"
#: lib/who_need_help_web/live/request_live/index.ex:34
#: lib/who_need_help_web/live/request_live/index.ex:47
#, elixir-autogen, elixir-format
msgid "Nearby help"
msgstr "Помощь рядом"
@ -1241,9 +1241,9 @@ msgid "Not for emergencies"
msgstr "Не для чрезвычайных ситуаций"
#: lib/who_need_help_web/live/notification_live.ex:748
#: lib/who_need_help_web/live/request_live/index.ex:311
#: lib/who_need_help_web/live/request_live/index.ex:312
#: lib/who_need_help_web/live/request_live/index.ex:423
#: lib/who_need_help_web/live/request_live/index.ex:397
#: lib/who_need_help_web/live/request_live/index.ex:398
#: lib/who_need_help_web/live/request_live/index.ex:509
#: lib/who_need_help_web/live/request_live/new.ex:449
#: lib/who_need_help_web/live/request_live/show.ex:943
#: lib/who_need_help_web/live/request_live/show.ex:944
@ -1251,7 +1251,7 @@ msgstr "Не для чрезвычайных ситуаций"
msgid "Now"
msgstr "Сейчас"
#: lib/who_need_help_web/live/activity_live/index.ex:367
#: lib/who_need_help_web/live/activity_live/index.ex:431
#, elixir-autogen, elixir-format
msgid "OPTIONAL SOCIAL PLANS"
msgstr "НЕОБЯЗАТЕЛЬНЫЕ СОВМЕСТНЫЕ ПЛАНЫ"
@ -1271,12 +1271,12 @@ msgstr ""
#: lib/who_need_help/notifications/text.ex:140
#: lib/who_need_help_web/controllers/content_removal_html.ex:21
#: lib/who_need_help_web/controllers/support_html.ex:16
#: lib/who_need_help_web/live/activity_live/index.ex:356
#: lib/who_need_help_web/live/activity_live/index.ex:420
#: lib/who_need_help_web/live/activity_live/show.ex:342
#: lib/who_need_help_web/live/moderation_live.ex:258
#: lib/who_need_help_web/live/report_live.ex:104
#: lib/who_need_help_web/live/request_live/index.ex:319
#: lib/who_need_help_web/live/request_live/index.ex:320
#: lib/who_need_help_web/live/request_live/index.ex:405
#: lib/who_need_help_web/live/request_live/index.ex:406
#: lib/who_need_help_web/live/request_live/show.ex:951
#: lib/who_need_help_web/live/support_operations_live.ex:237
#: lib/who_need_help_web/live/support_operations_live.ex:438
@ -1748,9 +1748,9 @@ msgid "Save profile"
msgstr "Сохранить профиль"
#: lib/who_need_help_web/live/notification_live.ex:750
#: lib/who_need_help_web/live/request_live/index.ex:315
#: lib/who_need_help_web/live/request_live/index.ex:316
#: lib/who_need_help_web/live/request_live/index.ex:425
#: lib/who_need_help_web/live/request_live/index.ex:401
#: lib/who_need_help_web/live/request_live/index.ex:402
#: lib/who_need_help_web/live/request_live/index.ex:511
#: lib/who_need_help_web/live/request_live/new.ex:451
#: lib/who_need_help_web/live/request_live/show.ex:947
#: lib/who_need_help_web/live/request_live/show.ex:948
@ -2022,7 +2022,7 @@ msgstr "Состояние этой активности уже изменило
msgid "This interaction is blocked."
msgstr "Это взаимодействие заблокировано."
#: lib/who_need_help_web/live/request_live/index.ex:363
#: lib/who_need_help_web/live/request_live/index.ex:449
#: lib/who_need_help_web/live/request_live/show.ex:1022
#, elixir-autogen, elixir-format
msgid "This is not an emergency service. Contact local emergency services for immediate danger."
@ -2075,9 +2075,9 @@ msgid "To see sent emails, visit"
msgstr "Чтобы посмотреть отправленные письма, откройте"
#: lib/who_need_help_web/live/notification_live.ex:749
#: lib/who_need_help_web/live/request_live/index.ex:313
#: lib/who_need_help_web/live/request_live/index.ex:314
#: lib/who_need_help_web/live/request_live/index.ex:424
#: lib/who_need_help_web/live/request_live/index.ex:399
#: lib/who_need_help_web/live/request_live/index.ex:400
#: lib/who_need_help_web/live/request_live/index.ex:510
#: lib/who_need_help_web/live/request_live/new.ex:450
#: lib/who_need_help_web/live/request_live/show.ex:945
#: lib/who_need_help_web/live/request_live/show.ex:946
@ -2152,7 +2152,7 @@ msgid "Update email instructions"
msgstr "Инструкции для изменения электронной почты"
#: lib/who_need_help_web/live/notification_live.ex:743
#: lib/who_need_help_web/live/request_live/index.ex:420
#: lib/who_need_help_web/live/request_live/index.ex:506
#: lib/who_need_help_web/live/request_live/new.ex:447
#, elixir-autogen, elixir-format
msgid "Urgency"
@ -2308,7 +2308,7 @@ msgstr ""
"через подписку ChatGPT разработчика. Ключ OpenAI API и резервный вариант API"
" с оплатой по использованию не применяются."
#: lib/who_need_help_web/live/request_live/index.ex:348
#: lib/who_need_help_web/live/request_live/index.ex:434
#, elixir-autogen, elixir-format
msgid "Who needs help?"
msgstr "Кому нужна помощь?"
@ -2432,7 +2432,7 @@ msgid "Your review is revealed only after both participants submit."
msgstr ""
"Ваш отзыв откроется только после того, как его отправят оба участника."
#: lib/who_need_help_web/live/request_live/index.ex:630
#: lib/who_need_help_web/live/request_live/index.ex:724
#, elixir-autogen, elixir-format
msgid "Your trust summary"
msgstr "Ваши показатели доверия"
@ -2453,17 +2453,17 @@ msgstr "сообщение активности"
msgid "before confirming."
msgstr "перед подтверждением."
#: lib/who_need_help_web/live/activity_live/index.ex:577
#: lib/who_need_help_web/live/activity_live/index.ex:646
#: lib/who_need_help_web/live/category_proposal_live.ex:252
#: lib/who_need_help_web/live/moderation_live.ex:372
#: lib/who_need_help_web/live/request_live/index.ex:564
#: lib/who_need_help_web/live/request_live/index.ex:655
#, elixir-autogen, elixir-format
msgid "by %{name}"
msgstr "автор: %{name}"
#: lib/who_need_help_web/live/profile_live.ex:190
#: lib/who_need_help_web/live/public_profile_live.ex:133
#: lib/who_need_help_web/live/request_live/index.ex:634
#: lib/who_need_help_web/live/request_live/index.ex:728
#: lib/who_need_help_web/live/request_live/show.ex:1374
#, elixir-autogen, elixir-format
msgid "completed"
@ -2499,7 +2499,7 @@ msgstr "организатор"
msgid "participants only"
msgstr "только участники"
#: lib/who_need_help_web/live/request_live/index.ex:638
#: lib/who_need_help_web/live/request_live/index.ex:732
#: lib/who_need_help_web/live/request_live/show.ex:1378
#, elixir-autogen, elixir-format
msgid "people"
@ -2559,7 +2559,7 @@ msgstr "срочно"
#: lib/who_need_help_web/live/activity_live/show.ex:719
#: lib/who_need_help_web/live/profile_live.ex:285
#: lib/who_need_help_web/live/public_profile_live.ex:98
#: lib/who_need_help_web/live/request_live/index.ex:642
#: lib/who_need_help_web/live/request_live/index.ex:736
#: lib/who_need_help_web/live/request_live/show.ex:1093
#: lib/who_need_help_web/live/request_live/show.ex:1476
#, elixir-autogen, elixir-format
@ -2592,8 +2592,8 @@ msgstr "← Заявки"
msgid "Approved"
msgstr "Одобрено"
#: lib/who_need_help_web/live/request_live/index.ex:329
#: lib/who_need_help_web/live/request_live/index.ex:330
#: lib/who_need_help_web/live/request_live/index.ex:415
#: lib/who_need_help_web/live/request_live/index.ex:416
#, elixir-autogen, elixir-format
msgid "Expired"
msgstr "Истекло"
@ -2646,8 +2646,8 @@ msgstr "Необычная частота действий"
msgid "Load earlier messages"
msgstr "Загрузить предыдущие сообщения"
#: lib/who_need_help_web/live/activity_live/index.ex:589
#: lib/who_need_help_web/live/activity_live/index.ex:679
#: lib/who_need_help_web/live/activity_live/index.ex:658
#: lib/who_need_help_web/live/activity_live/index.ex:751
#: lib/who_need_help_web/live/category_proposal_live.ex:284
#: lib/who_need_help_web/live/leaderboard_live.ex:91
#: lib/who_need_help_web/live/moderation_live.ex:465
@ -2659,7 +2659,7 @@ msgstr "Загрузить предыдущие сообщения"
#: lib/who_need_help_web/live/profile_live.ex:363
#: lib/who_need_help_web/live/public_profile_live.ex:178
#: lib/who_need_help_web/live/report_live.ex:98
#: lib/who_need_help_web/live/request_live/index.ex:699
#: lib/who_need_help_web/live/request_live/index.ex:805
#: lib/who_need_help_web/live/support_operations_live.ex:306
#: lib/who_need_help_web/live/support_operations_live.ex:505
#, elixir-autogen, elixir-format
@ -2667,7 +2667,7 @@ msgid "Load more"
msgstr "Загрузить ещё"
#: lib/who_need_help_web/live/activity_live/show.ex:544
#: lib/who_need_help_web/live/request_live/index.ex:610
#: lib/who_need_help_web/live/request_live/index.ex:701
#: lib/who_need_help_web/live/request_live/show.ex:1275
#, elixir-autogen, elixir-format
msgid "The map is unavailable in this browser. Request details remain usable."
@ -4656,7 +4656,7 @@ msgstr "Перетащите, чтобы переместить точную т
msgid "Exact point selected. Drag the red pin or click the map to move it."
msgstr "Точная точка выбрана. Перетащите красный маркер или нажмите на карту, чтобы переместить её."
#: lib/who_need_help_web/live/request_live/index.ex:453
#: lib/who_need_help_web/live/request_live/index.ex:539
#, elixir-autogen, elixir-format
msgid "%{count} request in this map area"
msgid_plural "%{count} requests in this map area"
@ -4664,37 +4664,37 @@ msgstr[0] "%{count} заявка в этой области карты"
msgstr[1] "%{count} заявки в этой области карты"
msgstr[2] "%{count} заявок в этой области карты"
#: lib/who_need_help_web/live/activity_live/index.ex:438
#: lib/who_need_help_web/live/request_live/index.ex:430
#: lib/who_need_help_web/live/activity_live/index.ex:502
#: lib/who_need_help_web/live/request_live/index.ex:516
#, elixir-autogen, elixir-format
msgid "Area or city"
msgstr "Район или город"
#: lib/who_need_help_web/live/activity_live/index.ex:525
#: lib/who_need_help_web/live/request_live/index.ex:515
#: lib/who_need_help_web/live/activity_live/index.ex:594
#: lib/who_need_help_web/live/request_live/index.ex:606
#, elixir-autogen, elixir-format
msgid "Choose an area on the map"
msgstr "Выберите область на карте"
#: lib/who_need_help_web/live/request_live/index.ex:350
#: lib/who_need_help_web/live/request_live/index.ex:436
#, elixir-autogen, elixir-format
msgid "Explore privacy-friendly areas and public exact meeting points chosen by requesters."
msgstr "Просматривайте безопасные для приватности области и точные публичные места встречи, выбранные авторами заявок."
#: lib/who_need_help_web/live/activity_live/index.ex:484
#: lib/who_need_help_web/live/request_live/index.ex:474
#: lib/who_need_help_web/live/activity_live/index.ex:553
#: lib/who_need_help_web/live/request_live/index.ex:565
#, elixir-autogen, elixir-format
msgid "List"
msgstr "Список"
#: lib/who_need_help_web/live/request_live/index.ex:586
#: lib/who_need_help_web/live/request_live/index.ex:677
#, elixir-autogen, elixir-format
msgid "Load more in this area"
msgstr "Загрузить ещё в этой области"
#: lib/who_need_help_web/components/layouts/root.html.heex:5
#: lib/who_need_help_web/live/activity_live/index.ex:504
#: lib/who_need_help_web/live/request_live/index.ex:494
#: lib/who_need_help_web/live/activity_live/index.ex:573
#: lib/who_need_help_web/live/request_live/index.ex:585
#, elixir-autogen, elixir-format
msgid "Map"
msgstr "Карта"
@ -4704,53 +4704,53 @@ msgstr "Карта"
msgid "Map preview"
msgstr "Предпросмотр карты"
#: lib/who_need_help_web/live/activity_live/index.ex:537
#: lib/who_need_help_web/live/request_live/index.ex:527
#: lib/who_need_help_web/live/activity_live/index.ex:606
#: lib/who_need_help_web/live/request_live/index.ex:618
#, elixir-autogen, elixir-format
msgid "Move or zoom the map to search somewhere else."
msgstr "Переместите или масштабируйте карту, чтобы искать в другом месте."
#: lib/who_need_help_web/live/activity_live/index.ex:471
#: lib/who_need_help_web/live/request_live/index.ex:461
#: lib/who_need_help_web/live/activity_live/index.ex:540
#: lib/who_need_help_web/live/request_live/index.ex:552
#, elixir-autogen, elixir-format
msgid "Move the map, then search the newly visible area."
msgstr "Переместите карту, затем выполните поиск в новой видимой области."
#: lib/who_need_help_web/live/request_live/index.ex:525
#: lib/who_need_help_web/live/request_live/index.ex:616
#, elixir-autogen, elixir-format
msgid "No open requests in this area"
msgstr "В этой области нет открытых заявок"
#: lib/who_need_help_web/live/request_live/index.ex:517
#: lib/who_need_help_web/live/request_live/index.ex:608
#, elixir-autogen, elixir-format
msgid "Only requests from the selected map area will be loaded."
msgstr "Будут загружены только заявки из выбранной области карты."
#: lib/who_need_help_web/components/layouts/root.html.heex:8
#: lib/who_need_help_web/live/request_live/index.ex:613
#: lib/who_need_help_web/live/request_live/index.ex:704
#, elixir-autogen, elixir-format
msgid "Open request"
msgstr "Открыть заявку"
#: lib/who_need_help_web/live/activity_live/index.ex:439
#: lib/who_need_help_web/live/request_live/index.ex:431
#: lib/who_need_help_web/live/activity_live/index.ex:503
#: lib/who_need_help_web/live/request_live/index.ex:517
#, elixir-autogen, elixir-format
msgid "Optional text search"
msgstr "Необязательный текстовый поиск"
#: lib/who_need_help_web/live/request_live/index.ex:592
#: lib/who_need_help_web/live/request_live/index.ex:683
#, elixir-autogen, elixir-format
msgid "Requests without map coordinates appear only when their public area description matches the text search."
msgstr "Заявки без координат на карте отображаются только тогда, когда описание публичной области совпадает с текстовым поиском."
#: lib/who_need_help_web/live/activity_live/index.ex:475
#: lib/who_need_help_web/live/request_live/index.ex:465
#: lib/who_need_help_web/live/activity_live/index.ex:544
#: lib/who_need_help_web/live/request_live/index.ex:556
#, elixir-autogen, elixir-format
msgid "Results layout"
msgstr "Расположение результатов"
#: lib/who_need_help_web/live/activity_live/index.ex:621
#: lib/who_need_help_web/live/request_live/index.ex:625
#: lib/who_need_help_web/live/activity_live/index.ex:693
#: lib/who_need_help_web/live/request_live/index.ex:719
#, elixir-autogen, elixir-format
msgid "Search this area"
msgstr "Искать в этой области"
@ -4760,21 +4760,21 @@ msgstr "Искать в этой области"
msgid "Static preview"
msgstr "Статический просмотр"
#: lib/who_need_help_web/live/activity_live/index.ex:494
#: lib/who_need_help_web/live/request_live/index.ex:484
#: lib/who_need_help_web/live/activity_live/index.ex:563
#: lib/who_need_help_web/live/request_live/index.ex:575
#, elixir-autogen, elixir-format
msgid "Together"
msgstr "Вместе"
#: lib/who_need_help_web/components/layouts/root.html.heex:6
#: lib/who_need_help_web/controllers/page_html/home.html.heex:160
#: lib/who_need_help_web/live/activity_live/index.ex:609
#: lib/who_need_help_web/live/activity_live/index.ex:681
#: lib/who_need_help_web/live/activity_live/new.ex:355
#: lib/who_need_help_web/live/activity_live/new.ex:414
#: lib/who_need_help_web/live/activity_live/show.ex:546
#: lib/who_need_help_web/live/notification_live.ex:602
#: lib/who_need_help_web/live/notification_live.ex:621
#: lib/who_need_help_web/live/request_live/index.ex:612
#: lib/who_need_help_web/live/request_live/index.ex:703
#: lib/who_need_help_web/live/request_live/new.ex:485
#: lib/who_need_help_web/live/request_live/new.ex:563
#: lib/who_need_help_web/live/request_live/show.ex:1277
@ -5164,7 +5164,7 @@ msgid "Nearby alert deleted."
msgstr "Уведомление поблизости удалено."
#: lib/who_need_help_web/live/notification_live.ex:456
#: lib/who_need_help_web/live/request_live/index.ex:376
#: lib/who_need_help_web/live/request_live/index.ex:462
#, elixir-autogen, elixir-format
msgid "Nearby requests"
msgstr "Заявки поблизости"
@ -5504,7 +5504,7 @@ msgid "WebGL is unavailable in this browser, so the full map cannot start. Selec
msgstr "WebGL недоступен в этом браузере, поэтому полная карта не может запуститься. Выберите пример ниже или попробуйте запустить карту ещё раз."
#: lib/who_need_help_web/components/layouts/root.html.heex:7
#: lib/who_need_help_web/live/activity_live/index.ex:610
#: lib/who_need_help_web/live/activity_live/index.ex:682
#: lib/who_need_help_web/live/activity_live/show.ex:547
#: lib/who_need_help_web/live/request_live/show.ex:1278
#, elixir-autogen, elixir-format
@ -6069,7 +6069,7 @@ msgstr "Теперь ваш отзыв виден"
msgid "Delivery settings"
msgstr "Настройки доставки"
#: lib/who_need_help_web/live/request_live/index.ex:569
#: lib/who_need_help_web/live/request_live/index.ex:660
#, elixir-autogen, elixir-format
msgid "Expires"
msgstr "Истекает"
@ -6089,12 +6089,12 @@ msgstr "Оповещения рядом"
msgid "Notification sections"
msgstr "Разделы уведомлений"
#: lib/who_need_help_web/live/activity_live/index.ex:605
#: lib/who_need_help_web/live/activity_live/index.ex:674
#, elixir-autogen, elixir-format
msgid "%{count} activities"
msgstr "Активностей: %{count}"
#: lib/who_need_help_web/live/activity_live/index.ex:463
#: lib/who_need_help_web/live/activity_live/index.ex:527
#, elixir-autogen, elixir-format
msgid "%{count} activity in this map area"
msgid_plural "%{count} activities in this map area"
@ -6102,17 +6102,17 @@ msgstr[0] "%{count} активность в этой области карты"
msgstr[1] "%{count} активности в этой области карты"
msgstr[2] "%{count} активностей в этой области карты"
#: lib/who_need_help_web/live/request_live/index.ex:614
#: lib/who_need_help_web/live/request_live/index.ex:705
#, elixir-autogen, elixir-format
msgid "%{count} requests"
msgstr "Заявок: %{count}"
#: lib/who_need_help_web/live/activity_live/index.ex:645
#: lib/who_need_help_web/live/activity_live/index.ex:717
#, elixir-autogen, elixir-format
msgid "Activities you create or ask to join will appear here. Leaving removes an activity from this list."
msgstr "Созданные вами активности и те, к которым вы попросили присоединиться, появятся здесь. После выхода активность исчезнет из списка."
#: lib/who_need_help_web/live/activity_live/index.ex:393
#: lib/who_need_help_web/live/activity_live/index.ex:457
#, elixir-autogen, elixir-format
msgid "Activity views"
msgstr "Разделы активностей"
@ -6127,12 +6127,12 @@ msgstr "Выберите категорию и понятно опишите з
msgid "Confirm the safety guidance before asking nearby people for help."
msgstr "Подтвердите правила безопасности перед публикацией заявки."
#: lib/who_need_help_web/live/request_live/index.ex:659
#: lib/who_need_help_web/live/request_live/index.ex:753
#, elixir-autogen, elixir-format
msgid "Create request"
msgstr "Создать заявку"
#: lib/who_need_help_web/live/request_live/index.ex:673
#: lib/who_need_help_web/live/request_live/index.ex:776
#, elixir-autogen, elixir-format
msgid "Create your first request"
msgstr "Создать первую заявку"
@ -6147,43 +6147,43 @@ msgstr "Что нужно сделать"
msgid "Location"
msgstr "Место"
#: lib/who_need_help_web/live/activity_live/index.ex:399
#: lib/who_need_help_web/live/activity_live/index.ex:463
#, elixir-autogen, elixir-format
msgid "My activities"
msgstr "Мои активности"
#: lib/who_need_help_web/live/request_live/index.ex:377
#: lib/who_need_help_web/live/request_live/index.ex:653
#: lib/who_need_help_web/live/request_live/index.ex:463
#: lib/who_need_help_web/live/request_live/index.ex:747
#, elixir-autogen, elixir-format
msgid "My requests"
msgstr "Мои заявки"
#: lib/who_need_help_web/live/activity_live/index.ex:398
#: lib/who_need_help_web/live/activity_live/index.ex:462
#, elixir-autogen, elixir-format
msgid "Nearby activities"
msgstr "Активности рядом"
#: lib/who_need_help_web/live/activity_live/index.ex:643
#: lib/who_need_help_web/live/activity_live/index.ex:715
#, elixir-autogen, elixir-format
msgid "No activities yet"
msgstr "Активностей пока нет"
#: lib/who_need_help_web/live/activity_live/index.ex:535
#: lib/who_need_help_web/live/activity_live/index.ex:604
#, elixir-autogen, elixir-format
msgid "No open activities in this area"
msgstr "В этой области нет открытых активностей"
#: lib/who_need_help_web/live/request_live/index.ex:668
#: lib/who_need_help_web/live/request_live/index.ex:771
#, elixir-autogen, elixir-format
msgid "No requests yet"
msgstr "Заявок пока нет"
#: lib/who_need_help_web/live/activity_live/index.ex:527
#: lib/who_need_help_web/live/activity_live/index.ex:596
#, elixir-autogen, elixir-format
msgid "Only activities from the selected map area will be loaded."
msgstr "Будут загружены только активности из выбранной области карты."
#: lib/who_need_help_web/live/activity_live/index.ex:604
#: lib/who_need_help_web/live/activity_live/index.ex:673
#, elixir-autogen, elixir-format
msgid "Open activity"
msgstr "Открыть активность"
@ -6193,12 +6193,12 @@ msgstr "Открыть активность"
msgid "Request form steps"
msgstr "Шаги создания заявки"
#: lib/who_need_help_web/live/request_live/index.ex:371
#: lib/who_need_help_web/live/request_live/index.ex:457
#, elixir-autogen, elixir-format
msgid "Request views"
msgstr "Разделы заявок"
#: lib/who_need_help_web/live/request_live/index.ex:670
#: lib/who_need_help_web/live/request_live/index.ex:773
#, elixir-autogen, elixir-format
msgid "Requests you publish or accept will appear here."
msgstr "Здесь появятся заявки, которые вы создали или приняли."
@ -6223,7 +6223,7 @@ msgstr "Укажите срочность и время закрытия зая
msgid "Share only as much location detail as helpers need."
msgstr "Покажите помощникам только необходимую информацию о месте."
#: lib/who_need_help_web/live/activity_live/index.ex:607
#: lib/who_need_help_web/live/activity_live/index.ex:679
#, elixir-autogen, elixir-format
msgid "The map is unavailable in this browser. Activity details remain usable."
msgstr "Карта недоступна в этом браузере. Информация об активностях по-прежнему доступна."
@ -6233,7 +6233,7 @@ msgstr "Карта недоступна в этом браузере. Инфор
msgid "Timing"
msgstr "Время"
#: lib/who_need_help_web/live/request_live/index.ex:655
#: lib/who_need_help_web/live/request_live/index.ex:749
#, elixir-autogen, elixir-format
msgid "Track requests you created and requests you accepted as a helper."
msgstr "Следите за созданными заявками и за теми, которые вы приняли как помощник."
@ -6894,11 +6894,32 @@ msgstr "Что должны видеть люди до одобрения?"
msgid "Choose the meeting point on the map or use your current location."
msgstr "Выберите место встречи на карте или используйте своё текущее местоположение."
#: lib/who_need_help_web/live/activity_live/index.ex:458
#: lib/who_need_help_web/live/activity_live/index.ex:517
#: lib/who_need_help_web/live/activity_live/index.ex:635
#: lib/who_need_help_web/live/request_live/index.ex:448
#: lib/who_need_help_web/live/request_live/index.ex:507
#: lib/who_need_help_web/live/activity_live/index.ex:522
#: lib/who_need_help_web/live/activity_live/index.ex:586
#: lib/who_need_help_web/live/activity_live/index.ex:707
#: lib/who_need_help_web/live/request_live/index.ex:534
#: lib/who_need_help_web/live/request_live/index.ex:598
#: lib/who_need_help_web/live/request_live/index.ex:763
#, elixir-autogen, elixir-format
msgid "Loading results…"
msgstr "Загружаем результаты…"
#: lib/who_need_help_web/live/activity_live/index.ex:676
#, elixir-autogen, elixir-format
msgid "Maximum zoom reached. Matching activities are listed beside the map."
msgstr "Достигнут максимальный масштаб. Подходящие мероприятия перечислены рядом с картой."
#: lib/who_need_help_web/live/request_live/index.ex:707
#, elixir-autogen, elixir-format
msgid "Maximum zoom reached. Matching requests are listed beside the map."
msgstr "Достигнут максимальный масштаб. Подходящие заявки перечислены рядом с картой."
#: lib/who_need_help_web/live/activity_live/index.ex:535
#, elixir-autogen, elixir-format
msgid "of %{count} matching activities with map locations"
msgstr "из %{count} подходящих мероприятий с местоположением на карте"
#: lib/who_need_help_web/live/request_live/index.ex:547
#, elixir-autogen, elixir-format
msgid "of %{count} matching requests with map locations"
msgstr "из %{count} подходящих заявок с местоположением на карте"

View File

@ -184,7 +184,7 @@ msgid "Active"
msgstr "Активний"
#: lib/who_need_help_web/components/layouts.ex:231
#: lib/who_need_help_web/live/activity_live/index.ex:33
#: lib/who_need_help_web/live/activity_live/index.ex:37
#, elixir-autogen, elixir-format
msgid "Activities"
msgstr "Активності"
@ -226,7 +226,7 @@ msgstr "Активність опубліковано. Ви схвалюєте
msgid "Activity restored."
msgstr "Активність відновлено."
#: lib/who_need_help_web/live/activity_live/index.ex:427
#: lib/who_need_help_web/live/activity_live/index.ex:491
#: lib/who_need_help_web/live/activity_live/new.ex:204
#, elixir-autogen, elixir-format
msgid "Activity type"
@ -243,12 +243,12 @@ msgstr "Додати непідтверджене посилання"
msgid "Administrator"
msgstr "Адміністратор"
#: lib/who_need_help_web/live/activity_live/index.ex:429
#: lib/who_need_help_web/live/activity_live/index.ex:493
#, elixir-autogen, elixir-format
msgid "All activities"
msgstr "Усі активності"
#: lib/who_need_help_web/live/request_live/index.ex:407
#: lib/who_need_help_web/live/request_live/index.ex:493
#, elixir-autogen, elixir-format
msgid "All categories"
msgstr "Усі категорії"
@ -263,7 +263,7 @@ msgstr "Уже зареєстровані?"
msgid "Another helper already accepted this request."
msgstr "Інший помічник уже прийняв цю заявку."
#: lib/who_need_help_web/live/request_live/index.ex:422
#: lib/who_need_help_web/live/request_live/index.ex:508
#, elixir-autogen, elixir-format
msgid "Any urgency"
msgstr "Будь-яка терміновість"
@ -383,16 +383,16 @@ msgstr "Скасувати активність"
msgid "Cancel request"
msgstr "Скасувати запит"
#: lib/who_need_help_web/live/activity_live/index.ex:358
#: lib/who_need_help_web/live/activity_live/index.ex:422
#: lib/who_need_help_web/live/activity_live/show.ex:344
#: lib/who_need_help_web/live/request_live/index.ex:327
#: lib/who_need_help_web/live/request_live/index.ex:328
#: lib/who_need_help_web/live/request_live/index.ex:413
#: lib/who_need_help_web/live/request_live/index.ex:414
#: lib/who_need_help_web/live/request_live/show.ex:956
#, elixir-autogen, elixir-format
msgid "Cancelled"
msgstr "Скасовано"
#: lib/who_need_help_web/live/request_live/index.ex:405
#: lib/who_need_help_web/live/request_live/index.ex:491
#: lib/who_need_help_web/live/request_live/new.ex:302
#, elixir-autogen, elixir-format
msgid "Category"
@ -476,7 +476,7 @@ msgstr "Закрити"
msgid "Coffee and conversation"
msgstr "Кава й розмова"
#: lib/who_need_help_web/live/activity_live/index.ex:372
#: lib/who_need_help_web/live/activity_live/index.ex:436
#, elixir-autogen, elixir-format
msgid "Coffee, cinema, walks, and hikes are separate from urgent help and never affect helper ratings."
msgstr ""
@ -493,19 +493,19 @@ msgstr "Коментар (необов’язково)"
msgid "Community helpers"
msgstr "Помічники спільноти"
#: lib/who_need_help_web/live/activity_live/index.ex:578
#: lib/who_need_help_web/live/activity_live/index.ex:647
#: lib/who_need_help_web/live/activity_live/show.ex:403
#: lib/who_need_help_web/live/activity_live/show.ex:639
#: lib/who_need_help_web/live/activity_live/show.ex:698
#: lib/who_need_help_web/live/request_live/index.ex:565
#: lib/who_need_help_web/live/request_live/index.ex:656
#, elixir-autogen, elixir-format
msgid "Community member"
msgstr "Член спільноти"
#: lib/who_need_help_web/live/activity_live/index.ex:357
#: lib/who_need_help_web/live/activity_live/index.ex:421
#: lib/who_need_help_web/live/activity_live/show.ex:343
#: lib/who_need_help_web/live/request_live/index.ex:325
#: lib/who_need_help_web/live/request_live/index.ex:326
#: lib/who_need_help_web/live/request_live/index.ex:411
#: lib/who_need_help_web/live/request_live/index.ex:412
#: lib/who_need_help_web/live/request_live/show.ex:955
#: lib/who_need_help_web/live/request_live/show.ex:974
#, elixir-autogen, elixir-format
@ -559,13 +559,13 @@ msgstr "Інструкції для підтвердження"
msgid "Confirming..."
msgstr "Підтвердження…"
#: lib/who_need_help_web/live/activity_live/index.ex:378
#: lib/who_need_help_web/live/activity_live/index.ex:442
#: lib/who_need_help_web/live/activity_live/new.ex:12
#, elixir-autogen, elixir-format
msgid "Create activity"
msgstr "Створити активність"
#: lib/who_need_help_web/live/activity_live/index.ex:650
#: lib/who_need_help_web/live/activity_live/index.ex:722
#: lib/who_need_help_web/live/activity_live/new.ex:180
#, elixir-autogen, elixir-format
msgid "Create an activity"
@ -576,7 +576,7 @@ msgstr "Створити активність"
msgid "Create category"
msgstr "Створити категорію"
#: lib/who_need_help_web/live/request_live/index.ex:356
#: lib/who_need_help_web/live/request_live/index.ex:442
#, elixir-autogen, elixir-format
msgid "Create urgent request"
msgstr "Створити термінову заявку"
@ -736,7 +736,7 @@ msgstr "Спливає:"
msgid "Explain what is already arranged and what the volunteer needs to do."
msgstr "Поясніть, що вже організовано й що має зробити волонтер."
#: lib/who_need_help_web/live/activity_live/index.ex:369
#: lib/who_need_help_web/live/activity_live/index.ex:433
#, elixir-autogen, elixir-format
msgid "Find people to join"
msgstr "Знайти компанію"
@ -936,8 +936,8 @@ msgstr ""
"Android показує постійне сповіщення й може продовжувати передавання після "
"згортання. Після зупинення передавання вихідні координати видаляються."
#: lib/who_need_help_web/live/request_live/index.ex:323
#: lib/who_need_help_web/live/request_live/index.ex:324
#: lib/who_need_help_web/live/request_live/index.ex:409
#: lib/who_need_help_web/live/request_live/index.ex:410
#: lib/who_need_help_web/live/request_live/show.ex:954
#, elixir-autogen, elixir-format
msgid "In progress"
@ -1065,8 +1065,8 @@ msgstr "Позначити завершеною"
msgid "Match and coordinate"
msgstr "Знайдіть помічника й узгодьте деталі"
#: lib/who_need_help_web/live/request_live/index.ex:321
#: lib/who_need_help_web/live/request_live/index.ex:322
#: lib/who_need_help_web/live/request_live/index.ex:407
#: lib/who_need_help_web/live/request_live/index.ex:408
#: lib/who_need_help_web/live/request_live/show.ex:952
#, elixir-autogen, elixir-format
msgid "Matched"
@ -1095,7 +1095,7 @@ msgstr ""
msgid "Medicine requests"
msgstr "Запити на ліки"
#: lib/who_need_help_web/live/activity_live/index.ex:385
#: lib/who_need_help_web/live/activity_live/index.ex:449
#, elixir-autogen, elixir-format
msgid "Meet in public first, keep exact details inside the approved group, and leave or report anything that feels unsafe."
msgstr ""
@ -1149,12 +1149,12 @@ msgstr "Модератор"
msgid "Moderator access is required."
msgstr "Потрібен доступ модератора."
#: lib/who_need_help_web/live/request_live/index.ex:347
#: lib/who_need_help_web/live/request_live/index.ex:433
#, elixir-autogen, elixir-format
msgid "NEARBY MUTUAL AID"
msgstr "ВЗАЄМОДОПОМОГА ПОРУЧ"
#: lib/who_need_help_web/live/request_live/index.ex:34
#: lib/who_need_help_web/live/request_live/index.ex:47
#, elixir-autogen, elixir-format
msgid "Nearby help"
msgstr "Допомога поруч"
@ -1237,9 +1237,9 @@ msgid "Not for emergencies"
msgstr "Не для екстрених ситуацій"
#: lib/who_need_help_web/live/notification_live.ex:748
#: lib/who_need_help_web/live/request_live/index.ex:311
#: lib/who_need_help_web/live/request_live/index.ex:312
#: lib/who_need_help_web/live/request_live/index.ex:423
#: lib/who_need_help_web/live/request_live/index.ex:397
#: lib/who_need_help_web/live/request_live/index.ex:398
#: lib/who_need_help_web/live/request_live/index.ex:509
#: lib/who_need_help_web/live/request_live/new.ex:449
#: lib/who_need_help_web/live/request_live/show.ex:943
#: lib/who_need_help_web/live/request_live/show.ex:944
@ -1247,7 +1247,7 @@ msgstr "Не для екстрених ситуацій"
msgid "Now"
msgstr "Зараз"
#: lib/who_need_help_web/live/activity_live/index.ex:367
#: lib/who_need_help_web/live/activity_live/index.ex:431
#, elixir-autogen, elixir-format
msgid "OPTIONAL SOCIAL PLANS"
msgstr "НЕОБОВ’ЯЗКОВІ СПІЛЬНІ ПЛАНИ"
@ -1267,12 +1267,12 @@ msgstr ""
#: lib/who_need_help/notifications/text.ex:140
#: lib/who_need_help_web/controllers/content_removal_html.ex:21
#: lib/who_need_help_web/controllers/support_html.ex:16
#: lib/who_need_help_web/live/activity_live/index.ex:356
#: lib/who_need_help_web/live/activity_live/index.ex:420
#: lib/who_need_help_web/live/activity_live/show.ex:342
#: lib/who_need_help_web/live/moderation_live.ex:258
#: lib/who_need_help_web/live/report_live.ex:104
#: lib/who_need_help_web/live/request_live/index.ex:319
#: lib/who_need_help_web/live/request_live/index.ex:320
#: lib/who_need_help_web/live/request_live/index.ex:405
#: lib/who_need_help_web/live/request_live/index.ex:406
#: lib/who_need_help_web/live/request_live/show.ex:951
#: lib/who_need_help_web/live/support_operations_live.ex:237
#: lib/who_need_help_web/live/support_operations_live.ex:438
@ -1745,9 +1745,9 @@ msgid "Save profile"
msgstr "Зберегти профіль"
#: lib/who_need_help_web/live/notification_live.ex:750
#: lib/who_need_help_web/live/request_live/index.ex:315
#: lib/who_need_help_web/live/request_live/index.ex:316
#: lib/who_need_help_web/live/request_live/index.ex:425
#: lib/who_need_help_web/live/request_live/index.ex:401
#: lib/who_need_help_web/live/request_live/index.ex:402
#: lib/who_need_help_web/live/request_live/index.ex:511
#: lib/who_need_help_web/live/request_live/new.ex:451
#: lib/who_need_help_web/live/request_live/show.ex:947
#: lib/who_need_help_web/live/request_live/show.ex:948
@ -2019,7 +2019,7 @@ msgstr "Цей стан активності вже змінено."
msgid "This interaction is blocked."
msgstr "Ця взаємодія заблокована."
#: lib/who_need_help_web/live/request_live/index.ex:363
#: lib/who_need_help_web/live/request_live/index.ex:449
#: lib/who_need_help_web/live/request_live/show.ex:1022
#, elixir-autogen, elixir-format
msgid "This is not an emergency service. Contact local emergency services for immediate danger."
@ -2072,9 +2072,9 @@ msgid "To see sent emails, visit"
msgstr "Щоб переглянути надіслані листи, відкрийте"
#: lib/who_need_help_web/live/notification_live.ex:749
#: lib/who_need_help_web/live/request_live/index.ex:313
#: lib/who_need_help_web/live/request_live/index.ex:314
#: lib/who_need_help_web/live/request_live/index.ex:424
#: lib/who_need_help_web/live/request_live/index.ex:399
#: lib/who_need_help_web/live/request_live/index.ex:400
#: lib/who_need_help_web/live/request_live/index.ex:510
#: lib/who_need_help_web/live/request_live/new.ex:450
#: lib/who_need_help_web/live/request_live/show.ex:945
#: lib/who_need_help_web/live/request_live/show.ex:946
@ -2149,7 +2149,7 @@ msgid "Update email instructions"
msgstr "Інструкції для зміни електронної пошти"
#: lib/who_need_help_web/live/notification_live.ex:743
#: lib/who_need_help_web/live/request_live/index.ex:420
#: lib/who_need_help_web/live/request_live/index.ex:506
#: lib/who_need_help_web/live/request_live/new.ex:447
#, elixir-autogen, elixir-format
msgid "Urgency"
@ -2304,7 +2304,7 @@ msgstr ""
"через підписку ChatGPT розробника. Ключ OpenAI API та резервний варіант API "
"з оплатою за використання не застосовуються."
#: lib/who_need_help_web/live/request_live/index.ex:348
#: lib/who_need_help_web/live/request_live/index.ex:434
#, elixir-autogen, elixir-format
msgid "Who needs help?"
msgstr "Хто потребує допомоги?"
@ -2426,7 +2426,7 @@ msgid "Your review is revealed only after both participants submit."
msgstr ""
"Ваш відгук відкриється лише після того, як його надішлють обидва учасники."
#: lib/who_need_help_web/live/request_live/index.ex:630
#: lib/who_need_help_web/live/request_live/index.ex:724
#, elixir-autogen, elixir-format
msgid "Your trust summary"
msgstr "Ваші показники довіри"
@ -2447,17 +2447,17 @@ msgstr "повідомлення активності"
msgid "before confirming."
msgstr "перед підтвердженням."
#: lib/who_need_help_web/live/activity_live/index.ex:577
#: lib/who_need_help_web/live/activity_live/index.ex:646
#: lib/who_need_help_web/live/category_proposal_live.ex:252
#: lib/who_need_help_web/live/moderation_live.ex:372
#: lib/who_need_help_web/live/request_live/index.ex:564
#: lib/who_need_help_web/live/request_live/index.ex:655
#, elixir-autogen, elixir-format
msgid "by %{name}"
msgstr "автор: %{name}"
#: lib/who_need_help_web/live/profile_live.ex:190
#: lib/who_need_help_web/live/public_profile_live.ex:133
#: lib/who_need_help_web/live/request_live/index.ex:634
#: lib/who_need_help_web/live/request_live/index.ex:728
#: lib/who_need_help_web/live/request_live/show.ex:1374
#, elixir-autogen, elixir-format
msgid "completed"
@ -2493,7 +2493,7 @@ msgstr "організатор"
msgid "participants only"
msgstr "лише учасники"
#: lib/who_need_help_web/live/request_live/index.ex:638
#: lib/who_need_help_web/live/request_live/index.ex:732
#: lib/who_need_help_web/live/request_live/show.ex:1378
#, elixir-autogen, elixir-format
msgid "people"
@ -2553,7 +2553,7 @@ msgstr "терміново"
#: lib/who_need_help_web/live/activity_live/show.ex:719
#: lib/who_need_help_web/live/profile_live.ex:285
#: lib/who_need_help_web/live/public_profile_live.ex:98
#: lib/who_need_help_web/live/request_live/index.ex:642
#: lib/who_need_help_web/live/request_live/index.ex:736
#: lib/who_need_help_web/live/request_live/show.ex:1093
#: lib/who_need_help_web/live/request_live/show.ex:1476
#, elixir-autogen, elixir-format
@ -2586,8 +2586,8 @@ msgstr "← Заявки"
msgid "Approved"
msgstr "Схвалено"
#: lib/who_need_help_web/live/request_live/index.ex:329
#: lib/who_need_help_web/live/request_live/index.ex:330
#: lib/who_need_help_web/live/request_live/index.ex:415
#: lib/who_need_help_web/live/request_live/index.ex:416
#, elixir-autogen, elixir-format
msgid "Expired"
msgstr "Спливло"
@ -2640,8 +2640,8 @@ msgstr "Незвична частота дій"
msgid "Load earlier messages"
msgstr "Завантажити попередні повідомлення"
#: lib/who_need_help_web/live/activity_live/index.ex:589
#: lib/who_need_help_web/live/activity_live/index.ex:679
#: lib/who_need_help_web/live/activity_live/index.ex:658
#: lib/who_need_help_web/live/activity_live/index.ex:751
#: lib/who_need_help_web/live/category_proposal_live.ex:284
#: lib/who_need_help_web/live/leaderboard_live.ex:91
#: lib/who_need_help_web/live/moderation_live.ex:465
@ -2653,7 +2653,7 @@ msgstr "Завантажити попередні повідомлення"
#: lib/who_need_help_web/live/profile_live.ex:363
#: lib/who_need_help_web/live/public_profile_live.ex:178
#: lib/who_need_help_web/live/report_live.ex:98
#: lib/who_need_help_web/live/request_live/index.ex:699
#: lib/who_need_help_web/live/request_live/index.ex:805
#: lib/who_need_help_web/live/support_operations_live.ex:306
#: lib/who_need_help_web/live/support_operations_live.ex:505
#, elixir-autogen, elixir-format
@ -2661,7 +2661,7 @@ msgid "Load more"
msgstr "Завантажити ще"
#: lib/who_need_help_web/live/activity_live/show.ex:544
#: lib/who_need_help_web/live/request_live/index.ex:610
#: lib/who_need_help_web/live/request_live/index.ex:701
#: lib/who_need_help_web/live/request_live/show.ex:1275
#, elixir-autogen, elixir-format
msgid "The map is unavailable in this browser. Request details remain usable."
@ -4650,7 +4650,7 @@ msgstr "Перетягніть, щоб перемістити точну точ
msgid "Exact point selected. Drag the red pin or click the map to move it."
msgstr "Точну точку вибрано. Перетягніть червоний маркер або натисніть на карту, щоб перемістити її."
#: lib/who_need_help_web/live/request_live/index.ex:453
#: lib/who_need_help_web/live/request_live/index.ex:539
#, elixir-autogen, elixir-format
msgid "%{count} request in this map area"
msgid_plural "%{count} requests in this map area"
@ -4658,37 +4658,37 @@ msgstr[0] "%{count} заявка в цій області карти"
msgstr[1] "%{count} заявки в цій області карти"
msgstr[2] "%{count} заявок у цій області карти"
#: lib/who_need_help_web/live/activity_live/index.ex:438
#: lib/who_need_help_web/live/request_live/index.ex:430
#: lib/who_need_help_web/live/activity_live/index.ex:502
#: lib/who_need_help_web/live/request_live/index.ex:516
#, elixir-autogen, elixir-format
msgid "Area or city"
msgstr "Район або місто"
#: lib/who_need_help_web/live/activity_live/index.ex:525
#: lib/who_need_help_web/live/request_live/index.ex:515
#: lib/who_need_help_web/live/activity_live/index.ex:594
#: lib/who_need_help_web/live/request_live/index.ex:606
#, elixir-autogen, elixir-format
msgid "Choose an area on the map"
msgstr "Виберіть область на карті"
#: lib/who_need_help_web/live/request_live/index.ex:350
#: lib/who_need_help_web/live/request_live/index.ex:436
#, elixir-autogen, elixir-format
msgid "Explore privacy-friendly areas and public exact meeting points chosen by requesters."
msgstr "Переглядайте приватні області та точні публічні місця зустрічі, вибрані авторами заявок."
#: lib/who_need_help_web/live/activity_live/index.ex:484
#: lib/who_need_help_web/live/request_live/index.ex:474
#: lib/who_need_help_web/live/activity_live/index.ex:553
#: lib/who_need_help_web/live/request_live/index.ex:565
#, elixir-autogen, elixir-format
msgid "List"
msgstr "Список"
#: lib/who_need_help_web/live/request_live/index.ex:586
#: lib/who_need_help_web/live/request_live/index.ex:677
#, elixir-autogen, elixir-format
msgid "Load more in this area"
msgstr "Завантажити ще в цій області"
#: lib/who_need_help_web/components/layouts/root.html.heex:5
#: lib/who_need_help_web/live/activity_live/index.ex:504
#: lib/who_need_help_web/live/request_live/index.ex:494
#: lib/who_need_help_web/live/activity_live/index.ex:573
#: lib/who_need_help_web/live/request_live/index.ex:585
#, elixir-autogen, elixir-format
msgid "Map"
msgstr "Карта"
@ -4698,53 +4698,53 @@ msgstr "Карта"
msgid "Map preview"
msgstr "Попередній перегляд карти"
#: lib/who_need_help_web/live/activity_live/index.ex:537
#: lib/who_need_help_web/live/request_live/index.ex:527
#: lib/who_need_help_web/live/activity_live/index.ex:606
#: lib/who_need_help_web/live/request_live/index.ex:618
#, elixir-autogen, elixir-format
msgid "Move or zoom the map to search somewhere else."
msgstr "Перемістіть або масштабуйте карту, щоб шукати в іншому місці."
#: lib/who_need_help_web/live/activity_live/index.ex:471
#: lib/who_need_help_web/live/request_live/index.ex:461
#: lib/who_need_help_web/live/activity_live/index.ex:540
#: lib/who_need_help_web/live/request_live/index.ex:552
#, elixir-autogen, elixir-format
msgid "Move the map, then search the newly visible area."
msgstr "Перемістіть карту, а потім виконайте пошук у новій видимій області."
#: lib/who_need_help_web/live/request_live/index.ex:525
#: lib/who_need_help_web/live/request_live/index.ex:616
#, elixir-autogen, elixir-format
msgid "No open requests in this area"
msgstr "У цій області немає відкритих заявок"
#: lib/who_need_help_web/live/request_live/index.ex:517
#: lib/who_need_help_web/live/request_live/index.ex:608
#, elixir-autogen, elixir-format
msgid "Only requests from the selected map area will be loaded."
msgstr "Буде завантажено лише заявки з вибраної області карти."
#: lib/who_need_help_web/components/layouts/root.html.heex:8
#: lib/who_need_help_web/live/request_live/index.ex:613
#: lib/who_need_help_web/live/request_live/index.ex:704
#, elixir-autogen, elixir-format
msgid "Open request"
msgstr "Відкрити заявку"
#: lib/who_need_help_web/live/activity_live/index.ex:439
#: lib/who_need_help_web/live/request_live/index.ex:431
#: lib/who_need_help_web/live/activity_live/index.ex:503
#: lib/who_need_help_web/live/request_live/index.ex:517
#, elixir-autogen, elixir-format
msgid "Optional text search"
msgstr "Необов’язковий текстовий пошук"
#: lib/who_need_help_web/live/request_live/index.ex:592
#: lib/who_need_help_web/live/request_live/index.ex:683
#, elixir-autogen, elixir-format
msgid "Requests without map coordinates appear only when their public area description matches the text search."
msgstr "Заявки без координат на карті з’являються лише тоді, коли опис публічної області збігається з текстовим пошуком."
#: lib/who_need_help_web/live/activity_live/index.ex:475
#: lib/who_need_help_web/live/request_live/index.ex:465
#: lib/who_need_help_web/live/activity_live/index.ex:544
#: lib/who_need_help_web/live/request_live/index.ex:556
#, elixir-autogen, elixir-format
msgid "Results layout"
msgstr "Розташування результатів"
#: lib/who_need_help_web/live/activity_live/index.ex:621
#: lib/who_need_help_web/live/request_live/index.ex:625
#: lib/who_need_help_web/live/activity_live/index.ex:693
#: lib/who_need_help_web/live/request_live/index.ex:719
#, elixir-autogen, elixir-format
msgid "Search this area"
msgstr "Шукати в цій області"
@ -4754,21 +4754,21 @@ msgstr "Шукати в цій області"
msgid "Static preview"
msgstr "Статичний перегляд"
#: lib/who_need_help_web/live/activity_live/index.ex:494
#: lib/who_need_help_web/live/request_live/index.ex:484
#: lib/who_need_help_web/live/activity_live/index.ex:563
#: lib/who_need_help_web/live/request_live/index.ex:575
#, elixir-autogen, elixir-format
msgid "Together"
msgstr "Разом"
#: lib/who_need_help_web/components/layouts/root.html.heex:6
#: lib/who_need_help_web/controllers/page_html/home.html.heex:160
#: lib/who_need_help_web/live/activity_live/index.ex:609
#: lib/who_need_help_web/live/activity_live/index.ex:681
#: lib/who_need_help_web/live/activity_live/new.ex:355
#: lib/who_need_help_web/live/activity_live/new.ex:414
#: lib/who_need_help_web/live/activity_live/show.ex:546
#: lib/who_need_help_web/live/notification_live.ex:602
#: lib/who_need_help_web/live/notification_live.ex:621
#: lib/who_need_help_web/live/request_live/index.ex:612
#: lib/who_need_help_web/live/request_live/index.ex:703
#: lib/who_need_help_web/live/request_live/new.ex:485
#: lib/who_need_help_web/live/request_live/new.ex:563
#: lib/who_need_help_web/live/request_live/show.ex:1277
@ -5158,7 +5158,7 @@ msgid "Nearby alert deleted."
msgstr "Сповіщення поблизу видалено."
#: lib/who_need_help_web/live/notification_live.ex:456
#: lib/who_need_help_web/live/request_live/index.ex:376
#: lib/who_need_help_web/live/request_live/index.ex:462
#, elixir-autogen, elixir-format
msgid "Nearby requests"
msgstr "Заявки поблизу"
@ -5498,7 +5498,7 @@ msgid "WebGL is unavailable in this browser, so the full map cannot start. Selec
msgstr "WebGL недоступний у цьому браузері, тому повна карта не може запуститися. Виберіть приклад нижче або спробуйте запустити карту ще раз."
#: lib/who_need_help_web/components/layouts/root.html.heex:7
#: lib/who_need_help_web/live/activity_live/index.ex:610
#: lib/who_need_help_web/live/activity_live/index.ex:682
#: lib/who_need_help_web/live/activity_live/show.ex:547
#: lib/who_need_help_web/live/request_live/show.ex:1278
#, elixir-autogen, elixir-format
@ -6063,7 +6063,7 @@ msgstr "Тепер ваш відгук видно"
msgid "Delivery settings"
msgstr "Налаштування доставки"
#: lib/who_need_help_web/live/request_live/index.ex:569
#: lib/who_need_help_web/live/request_live/index.ex:660
#, elixir-autogen, elixir-format
msgid "Expires"
msgstr "Спливає"
@ -6083,12 +6083,12 @@ msgstr "Сповіщення поруч"
msgid "Notification sections"
msgstr "Розділи сповіщень"
#: lib/who_need_help_web/live/activity_live/index.ex:605
#: lib/who_need_help_web/live/activity_live/index.ex:674
#, elixir-autogen, elixir-format
msgid "%{count} activities"
msgstr "Активностей: %{count}"
#: lib/who_need_help_web/live/activity_live/index.ex:463
#: lib/who_need_help_web/live/activity_live/index.ex:527
#, elixir-autogen, elixir-format
msgid "%{count} activity in this map area"
msgid_plural "%{count} activities in this map area"
@ -6096,17 +6096,17 @@ msgstr[0] "%{count} активність у цій області карти"
msgstr[1] "%{count} активності в цій області карти"
msgstr[2] "%{count} активностей у цій області карти"
#: lib/who_need_help_web/live/request_live/index.ex:614
#: lib/who_need_help_web/live/request_live/index.ex:705
#, elixir-autogen, elixir-format
msgid "%{count} requests"
msgstr "Запитів: %{count}"
#: lib/who_need_help_web/live/activity_live/index.ex:645
#: lib/who_need_help_web/live/activity_live/index.ex:717
#, elixir-autogen, elixir-format
msgid "Activities you create or ask to join will appear here. Leaving removes an activity from this list."
msgstr "Створені вами активності й ті, до яких ви попросили приєднатися, з’являться тут. Після виходу активність зникне зі списку."
#: lib/who_need_help_web/live/activity_live/index.ex:393
#: lib/who_need_help_web/live/activity_live/index.ex:457
#, elixir-autogen, elixir-format
msgid "Activity views"
msgstr "Розділи активностей"
@ -6121,12 +6121,12 @@ msgstr "Оберіть категорію та зрозуміло опишіть
msgid "Confirm the safety guidance before asking nearby people for help."
msgstr "Підтвердьте правила безпеки перед публікацією запиту."
#: lib/who_need_help_web/live/request_live/index.ex:659
#: lib/who_need_help_web/live/request_live/index.ex:753
#, elixir-autogen, elixir-format
msgid "Create request"
msgstr "Створити запит"
#: lib/who_need_help_web/live/request_live/index.ex:673
#: lib/who_need_help_web/live/request_live/index.ex:776
#, elixir-autogen, elixir-format
msgid "Create your first request"
msgstr "Створити перший запит"
@ -6141,43 +6141,43 @@ msgstr "Що потрібно зробити"
msgid "Location"
msgstr "Місце"
#: lib/who_need_help_web/live/activity_live/index.ex:399
#: lib/who_need_help_web/live/activity_live/index.ex:463
#, elixir-autogen, elixir-format
msgid "My activities"
msgstr "Мої активності"
#: lib/who_need_help_web/live/request_live/index.ex:377
#: lib/who_need_help_web/live/request_live/index.ex:653
#: lib/who_need_help_web/live/request_live/index.ex:463
#: lib/who_need_help_web/live/request_live/index.ex:747
#, elixir-autogen, elixir-format
msgid "My requests"
msgstr "Мої запити"
#: lib/who_need_help_web/live/activity_live/index.ex:398
#: lib/who_need_help_web/live/activity_live/index.ex:462
#, elixir-autogen, elixir-format
msgid "Nearby activities"
msgstr "Активності поруч"
#: lib/who_need_help_web/live/activity_live/index.ex:643
#: lib/who_need_help_web/live/activity_live/index.ex:715
#, elixir-autogen, elixir-format
msgid "No activities yet"
msgstr "Активностей поки немає"
#: lib/who_need_help_web/live/activity_live/index.ex:535
#: lib/who_need_help_web/live/activity_live/index.ex:604
#, elixir-autogen, elixir-format
msgid "No open activities in this area"
msgstr "У цій області немає відкритих активностей"
#: lib/who_need_help_web/live/request_live/index.ex:668
#: lib/who_need_help_web/live/request_live/index.ex:771
#, elixir-autogen, elixir-format
msgid "No requests yet"
msgstr "Запитів поки немає"
#: lib/who_need_help_web/live/activity_live/index.ex:527
#: lib/who_need_help_web/live/activity_live/index.ex:596
#, elixir-autogen, elixir-format
msgid "Only activities from the selected map area will be loaded."
msgstr "Буде завантажено лише активності з вибраної області карти."
#: lib/who_need_help_web/live/activity_live/index.ex:604
#: lib/who_need_help_web/live/activity_live/index.ex:673
#, elixir-autogen, elixir-format
msgid "Open activity"
msgstr "Відкрити активність"
@ -6187,12 +6187,12 @@ msgstr "Відкрити активність"
msgid "Request form steps"
msgstr "Кроки створення запиту"
#: lib/who_need_help_web/live/request_live/index.ex:371
#: lib/who_need_help_web/live/request_live/index.ex:457
#, elixir-autogen, elixir-format
msgid "Request views"
msgstr "Розділи запитів"
#: lib/who_need_help_web/live/request_live/index.ex:670
#: lib/who_need_help_web/live/request_live/index.ex:773
#, elixir-autogen, elixir-format
msgid "Requests you publish or accept will appear here."
msgstr "Тут з’являться запити, які ви створили або прийняли."
@ -6217,7 +6217,7 @@ msgstr "Укажіть терміновість і час закриття за
msgid "Share only as much location detail as helpers need."
msgstr "Покажіть помічникам лише необхідну інформацію про місце."
#: lib/who_need_help_web/live/activity_live/index.ex:607
#: lib/who_need_help_web/live/activity_live/index.ex:679
#, elixir-autogen, elixir-format
msgid "The map is unavailable in this browser. Activity details remain usable."
msgstr "Карта недоступна в цьому браузері. Інформація про активності залишається доступною."
@ -6227,7 +6227,7 @@ msgstr "Карта недоступна в цьому браузері. Інфо
msgid "Timing"
msgstr "Час"
#: lib/who_need_help_web/live/request_live/index.ex:655
#: lib/who_need_help_web/live/request_live/index.ex:749
#, elixir-autogen, elixir-format
msgid "Track requests you created and requests you accepted as a helper."
msgstr "Стежте за створеними запитами й тими, які ви прийняли як помічник."
@ -6888,11 +6888,32 @@ msgstr "Що мають бачити люди до схвалення?"
msgid "Choose the meeting point on the map or use your current location."
msgstr "Оберіть місце зустрічі на карті або скористайтеся своїм поточним місцезнаходженням."
#: lib/who_need_help_web/live/activity_live/index.ex:458
#: lib/who_need_help_web/live/activity_live/index.ex:517
#: lib/who_need_help_web/live/activity_live/index.ex:635
#: lib/who_need_help_web/live/request_live/index.ex:448
#: lib/who_need_help_web/live/request_live/index.ex:507
#: lib/who_need_help_web/live/activity_live/index.ex:522
#: lib/who_need_help_web/live/activity_live/index.ex:586
#: lib/who_need_help_web/live/activity_live/index.ex:707
#: lib/who_need_help_web/live/request_live/index.ex:534
#: lib/who_need_help_web/live/request_live/index.ex:598
#: lib/who_need_help_web/live/request_live/index.ex:763
#, elixir-autogen, elixir-format
msgid "Loading results…"
msgstr "Завантажуємо результати…"
#: lib/who_need_help_web/live/activity_live/index.ex:676
#, elixir-autogen, elixir-format
msgid "Maximum zoom reached. Matching activities are listed beside the map."
msgstr "Досягнуто максимального масштабу. Відповідні заходи перелічено поруч із картою."
#: lib/who_need_help_web/live/request_live/index.ex:707
#, elixir-autogen, elixir-format
msgid "Maximum zoom reached. Matching requests are listed beside the map."
msgstr "Досягнуто максимального масштабу. Відповідні заявки перелічено поруч із картою."
#: lib/who_need_help_web/live/activity_live/index.ex:535
#, elixir-autogen, elixir-format
msgid "of %{count} matching activities with map locations"
msgstr "із %{count} відповідних заходів із місцезнаходженням на карті"
#: lib/who_need_help_web/live/request_live/index.ex:547
#, elixir-autogen, elixir-format
msgid "of %{count} matching requests with map locations"
msgstr "із %{count} відповідних заявок із місцезнаходженням на карті"

View File

@ -448,6 +448,11 @@ defmodule WhoNeedHelp.ActivitiesTest do
cluster = Enum.find(items, &match?(%{type: "cluster", count: 3}, &1))
assert cluster
assert cluster.point_count == 3
assert cluster.hierarchy_level == 11
assert String.starts_with?(cluster.cluster_id, "activity:11:")
assert String.starts_with?(cluster.parent_cluster_id, "activity:10:")
assert cluster.expansion_zoom > 9
assert_in_delta cluster.latitude, 50.45, 0.000_001
assert_in_delta cluster.longitude, 30.52, 0.000_001
assert_in_delta cluster.bounds.west, 30.52, 0.000_001
@ -456,9 +461,33 @@ defmodule WhoNeedHelp.ActivitiesTest do
assert_in_delta cluster.bounds.north, 50.45, 0.000_001
assert Enum.any?(items, fn
%{type: "activity", id: id} -> id == sparse.id
_item -> false
%{
type: "activity",
id: id,
hierarchy_level: 11,
parent_cluster_id: "activity:10:" <> _
} ->
id == sparse.id
_item ->
false
end)
world_items =
Activities.map_discovery_items(
context.outsider_scope,
%{},
viewport!(-179.999, -85.0, 179.999, 85.0, 0)
)
represented_count =
Enum.reduce(world_items, 0, fn
%{type: "cluster", count: count}, total -> total + count
_point, total -> total + 1
end)
assert represented_count ==
Activities.count_map_discovery_items(context.outsider_scope, %{})
end
test "scale fixture activities keep distinct synthetic public map coordinates", context do

View File

@ -0,0 +1,43 @@
defmodule WhoNeedHelp.Help.DiscoveryClusterTest do
use ExUnit.Case, async: true
alias WhoNeedHelp.Help.DiscoveryCluster
test "map zooms select stable hierarchy levels" do
assert DiscoveryCluster.level_for_zoom(0) == 2
assert DiscoveryCluster.level_for_zoom(5.49) == 7
assert DiscoveryCluster.level_for_zoom(5.99) == 7
assert DiscoveryCluster.level_for_zoom(6) == 8
assert DiscoveryCluster.level_for_zoom(22) == 24
assert_in_delta(
DiscoveryCluster.cell_size_meters(8),
DiscoveryCluster.cell_size_meters(7) / 2,
0.000_001
)
end
test "cluster ids and parents form a deterministic quadtree" do
assert DiscoveryCluster.cluster_id(:request, 8, 123, 456) == "request:8:123:456"
assert DiscoveryCluster.parent_id(:request, 8, 123, 456) == "request:7:61:228"
assert DiscoveryCluster.parent_id(:activity, 0, 0, 0) == nil
end
test "expansion zoom is the first child level that separates member bounds" do
level = 7
cell = DiscoveryCluster.cell_size_meters(level)
west = -DiscoveryCluster.world_half_meters() + cell * 10 + cell * 0.1
south = -DiscoveryCluster.world_half_meters() + cell * 20 + cell * 0.1
assert DiscoveryCluster.expansion_zoom(
level,
west,
south,
west + cell * 0.6,
south + cell * 0.6
) == 6
assert DiscoveryCluster.expansion_zoom(level, west, south, west, south) == 22
assert DiscoveryCluster.expansion_zoom(24, west, south, west, south) == 22
end
end

View File

@ -109,47 +109,6 @@ defmodule WhoNeedHelp.Help.DiscoveryViewportTest do
end)
end
test "derives cluster detail from both geographic bounds and rendered pixels" do
{:ok, city} =
DiscoveryViewport.cast(%{
west: 30,
south: 50,
east: 31,
north: 51,
zoom: 9,
width: 800,
height: 600
})
{:ok, neighborhood} =
DiscoveryViewport.cast(%{
west: 30.4,
south: 50.4,
east: 30.6,
north: 50.6,
zoom: 13,
width: 800,
height: 600
})
{:ok, wide_canvas} =
DiscoveryViewport.cast(%{
west: 30,
south: 50,
east: 31,
north: 51,
zoom: 9,
width: 1600,
height: 1200
})
assert DiscoveryViewport.cluster_cell_meters(city) >
DiscoveryViewport.cluster_cell_meters(neighborhood)
assert DiscoveryViewport.cluster_cell_meters(city) >
DiscoveryViewport.cluster_cell_meters(wide_canvas)
end
test "serializes stable URL parameters and compares visible areas" do
{:ok, viewport} =
DiscoveryViewport.cast(%{

View File

@ -564,6 +564,11 @@ defmodule WhoNeedHelp.MutualAidFlowTest do
cluster = Enum.find(items, &match?(%{type: "cluster", count: 3}, &1))
assert cluster
assert cluster.point_count == 3
assert cluster.hierarchy_level == 11
assert String.starts_with?(cluster.cluster_id, "request:11:")
assert String.starts_with?(cluster.parent_cluster_id, "request:10:")
assert cluster.expansion_zoom > viewport.zoom
assert_in_delta cluster.latitude, 50.4501, 0.000_001
assert_in_delta cluster.longitude, 30.52345, 0.000_001
assert_in_delta cluster.bounds.west, 30.5234, 0.000_001
@ -572,8 +577,17 @@ defmodule WhoNeedHelp.MutualAidFlowTest do
assert_in_delta cluster.bounds.north, 50.4501, 0.000_001
assert Enum.any?(items, fn
%{type: "request", id: id, exact: true} -> id == sparse_request.id
_item -> false
%{
type: "request",
id: id,
exact: true,
hierarchy_level: 11,
parent_cluster_id: "request:10:" <> _
} ->
id == sparse_request.id
_item ->
false
end)
dense_ids = MapSet.new(dense_requests, & &1.id)
@ -582,6 +596,21 @@ defmodule WhoNeedHelp.MutualAidFlowTest do
%{type: "request", id: id} -> MapSet.member?(dense_ids, id)
_item -> false
end)
world_items =
Help.map_discovery_items(
context.helper_scope,
%{},
viewport!(-179.999, -85.0, 179.999, 85.0, 0)
)
represented_count =
Enum.reduce(world_items, 0, fn
%{type: "cluster", count: count}, total -> total + count
_point, total -> total + 1
end)
assert represented_count == Help.count_map_discovery_items(context.helper_scope, %{})
end
test "map discovery does not expose an exact-for-match coordinate", context do