Key Cities: every string on screen comes from the catalogue
22 English strings outside the catalogue. 10 were dead duplicates: the filter, tab and band labels were already translated from keyCities.* in the templates, so the English copies go. The rest now come from keys:
- the list's caveat is three translated lines, and its long sentence is ONE key rendered with useSentence, with the named-centre count and "Nominatim/OpenStreetMap" passed in as values (the name as its own left-to-right island, never translated);
- a row's group count reuses keyCities.list.peopleGroups, and the unknown internet pill reuses keyCities.map.unknown;
- a load failure reads "The data could not be loaded: {detail}" in the reader's language, with the server's own words as the detail (list and tables);
- the poster wall's intro and its "Other" region come from keys.
Headless, SSR with the real catalogue (_qa-cycle/exp-shepherd/check-keycities-i18n.mjs): the caveats render from keyCities in en, ar and zh, the gazetteer's name stays a <bdi dir="ltr"> island, no raw key or placeholder. The experimental English scan: key-cities 22 → 0. key-cities stays gated (dev only); it builds.
This commit is contained in:
parent
10275c36f8
commit
70b64e9608
4 changed files with 33 additions and 33 deletions
|
|
@ -26,7 +26,9 @@
|
|||
* PERSECUTION-SAFETY: locations of PEOPLE GROUPS, which is public ethnographic
|
||||
* data. No church, believer or meeting-point information, and it must not gain any.
|
||||
*/
|
||||
import { computed, inject, ref, onMounted } from 'vue'
|
||||
import { computed, inject, ref, onMounted, h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { sentenceParts, keep, ltr } from '@map/composables/useSentence.js'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||
|
||||
|
|
@ -40,7 +42,7 @@ const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
|
|||
// it and block first paint. Measured on the other two apps: static imports took
|
||||
// them to 2.0MB and 2.9MB before this fix.
|
||||
const table = ref({ rows: [] })
|
||||
const loadErr = ref(null)
|
||||
const loadErr = ref(null) // the engine's or the server's own words: shown INSIDE a translated line
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const r = await fetch(`${assetsBase.value}/tables/key-cities.json`)
|
||||
|
|
@ -55,11 +57,8 @@ const named = computed(() => rows.value.filter((r) => r.named))
|
|||
// Default to the NAMED population centres: a first screen of 1,369 unnamed
|
||||
// single-group clusters would read as broken rather than as honest.
|
||||
const filter = ref('named')
|
||||
const FILTERS = [
|
||||
{ id: 'named', label: 'Named population centres' },
|
||||
{ id: 'multi', label: 'Multi-group clusters' },
|
||||
{ id: 'all', label: 'All locations' },
|
||||
]
|
||||
// Labels are keyCities.list.filters.<id> (the template translates them).
|
||||
const FILTERS = [{ id: 'named' }, { id: 'multi' }, { id: 'all' }]
|
||||
|
||||
const shown = computed(() => {
|
||||
if (filter.value === 'named') return named.value
|
||||
|
|
@ -87,6 +86,12 @@ const collapsed = ref({})
|
|||
const isShut = (n) => !!collapsed.value[n]
|
||||
const toggle = (n) => { collapsed.value = { ...collapsed.value, [n]: !collapsed.value[n] } }
|
||||
const fmt = (n) => (Number(n) || 0).toLocaleString()
|
||||
const { t } = useI18n()
|
||||
// ONE key for the caveat's sentence: the count and the gazetteer's name go in as values, the name as
|
||||
// its own left-to-right island (it is a proper name, never translated), so each language orders it.
|
||||
const caveatRow = computed(() => h('span', sentenceParts(
|
||||
t('keyCities.list.caveat.row', keep('n', 'source')),
|
||||
{ n: fmt(named.value.length), source: ltr('Nominatim/OpenStreetMap', true) })))
|
||||
|
||||
useShadowStyles(
|
||||
'.kc{position:absolute;inset:0;overflow-y:auto;padding:20px 18px 44px;background:#f6f7f9;color:#0f1419;' +
|
||||
|
|
@ -139,7 +144,7 @@ const band = (a) => a === 'yes' ? 'ok' : a === 'partial' ? 'mid' : a === 'no' ?
|
|||
{{ $t('keyCities.list.intro', { named: fmt(named.length), clusters: fmt(rows.length) }) }}
|
||||
</p>
|
||||
|
||||
<div v-if="loadErr" class="caveat">{{ loadErr }}</div>
|
||||
<div v-if="loadErr" class="caveat" role="alert">{{ $t('keyCities.loadFailed', { detail: loadErr }) }}</div>
|
||||
|
||||
<div class="kc-tabs">
|
||||
<button v-for="f in FILTERS" :key="f.id" type="button"
|
||||
|
|
@ -167,29 +172,24 @@ const band = (a) => a === 'yes' ? 'ok' : a === 'partial' ? 'mid' : a === 'no' ?
|
|||
</div>
|
||||
<div class="r-meta">
|
||||
{{ r.country }}<template v-if="r.admin_state"> · {{ r.admin_state }}</template>
|
||||
· {{ r.people_group_count }} group{{ r.people_group_count === 1 ? '' : 's' }}
|
||||
· {{ $t('keyCities.list.peopleGroups', { n: fmt(r.people_group_count) }) }}
|
||||
<template v-if="r.languages"> · {{ r.languages.split(', ').slice(0, 3).join(', ') }}</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="r-num">{{ fmt(r.upg_population) }}</div>
|
||||
<span class="pill" :class="band(r.country_internet_access)">
|
||||
{{ r.country_internet_pct != null ? r.country_internet_pct + '%' : 'unknown' }}
|
||||
{{ r.country_internet_pct != null ? r.country_internet_pct + '%' : $t('keyCities.map.unknown') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="caveat">
|
||||
<b>What a row is.</b> A cluster of people-group centroids within ~25km — <b>not</b> a municipal
|
||||
boundary. Pins mark where a people group is centred, not a settlement, so clusters are never
|
||||
named by inference: the {{ fmt(named.length) }} multi-group centres were resolved by
|
||||
<b>Nominatim/OpenStreetMap</b> (ODbL, cited per row) and the rest stay unnamed rather than guessed.
|
||||
<b>{{ $t('keyCities.list.caveat.rowLead') }}</b> <component :is="caveatRow" />
|
||||
<br><br>
|
||||
<b>Internet figures are NATIONAL</b>, shown as context. Per-city connectivity is not publicly
|
||||
available at this scale, and a national figure must not be read as a city measurement.
|
||||
<b>{{ $t('keyCities.map.national') }}</b> {{ $t('keyCities.list.caveat.internet') }}
|
||||
<br><br>
|
||||
This page carries people-group locations and connectivity only — no church, believer or
|
||||
meeting-point data, by design.
|
||||
{{ $t('keyCities.list.caveat.scope') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -66,10 +66,11 @@ const selected = ref(null)
|
|||
const err = ref(null)
|
||||
|
||||
// Bands, not a continuous ramp: a reader can hold four categories, not a gradient.
|
||||
// Labels are keyCities.map.bands.<id> (the legend translates them).
|
||||
const BANDS = [
|
||||
{ id: 'named', label: 'named centre', color: '#0b7a5b' },
|
||||
{ id: 'multi', label: 'multi-group', color: '#e07a2b' },
|
||||
{ id: 'single', label: 'single group', color: '#9aa2ad' },
|
||||
{ id: 'named', color: '#0b7a5b' },
|
||||
{ id: 'multi', color: '#e07a2b' },
|
||||
{ id: 'single', color: '#9aa2ad' },
|
||||
]
|
||||
const bandColor = (b) => (BANDS.find((x) => x.id === b) || {}).color || '#9aa2ad'
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
* Partial and correct beats complete and wrong. The wall shows only what exists.
|
||||
*/
|
||||
import { computed, inject, ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PosterWall from '@map/components/poster/PosterWall.vue'
|
||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||
|
||||
|
|
@ -46,12 +47,9 @@ const items = computed(() => rows.value
|
|||
bbox: null,
|
||||
})))
|
||||
|
||||
const groupBy = (c) => c.region || 'Other'
|
||||
const { t } = useI18n()
|
||||
const groupBy = (c) => c.region || t('keyCities.posters.otherRegion')
|
||||
const collapsedBy = (sec) => sec.total < 20
|
||||
|
||||
const intro =
|
||||
'A printable sheet for each population centre where unengaged people groups live — ' +
|
||||
'named from a cited gazetteer, never inferred.'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -63,7 +61,7 @@ const intro =
|
|||
event-name="doxa-key-cities:show-on-map"
|
||||
:heading="$t('keyCities.posters.heading')"
|
||||
:tag="$t('keyCities.posters.tag')"
|
||||
:intro="intro"
|
||||
:intro="$t('keyCities.posters.intro')"
|
||||
unit="UPG"
|
||||
:item-noun="$t('keyCities.posters.itemNoun')"
|
||||
:item-noun-plural="$t('keyCities.posters.itemNounPlural')"
|
||||
|
|
|
|||
|
|
@ -28,14 +28,15 @@ const profileConfig = inject('profileConfig', ref(null))
|
|||
const isDark = computed(() => profileConfig.value?.isDark === true)
|
||||
const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
|
||||
|
||||
// Tab labels are keyCities.tables.tabs.<id> (the template translates them).
|
||||
const TABS = [
|
||||
{ id: 'cities', label: 'Key Cities', file: 'key-cities' },
|
||||
{ id: 'cities', file: 'key-cities' },
|
||||
// PER-CITY languages: 30 clusters have a bridge language spoken RIGHT THERE
|
||||
// that already carries a resource while the group's own language does not.
|
||||
{ id: 'langcity', label: 'Languages Here', file: 'languages-per-city' },
|
||||
{ id: 'languages', label: 'By Country', file: 'languages-by-country' },
|
||||
{ id: 'operators', label: 'ISPs & Carriers', file: 'network-operators' },
|
||||
{ id: 'messaging', label: 'Messaging Apps', file: 'messaging-channels' },
|
||||
{ id: 'langcity', file: 'languages-per-city' },
|
||||
{ id: 'languages', file: 'languages-by-country' },
|
||||
{ id: 'operators', file: 'network-operators' },
|
||||
{ id: 'messaging', file: 'messaging-channels' },
|
||||
// Field-access/visa data is PRIVATE (kept outside the repo — release governance):
|
||||
// publishing it could accelerate access closures. Tab removed from the
|
||||
// public build; the dataset stays available for internal analysis.
|
||||
|
|
@ -98,7 +99,7 @@ useShadowStyles(
|
|||
<!-- :key forces a fresh Tabulator per tab rather than mutating one in place;
|
||||
swapping both columns AND data on a live instance is where column state
|
||||
(frozen, widths, filters) desynchronises from the new schema. -->
|
||||
<div v-if="loadErr" class="ct-err">{{ loadErr }}</div>
|
||||
<div v-if="loadErr" class="ct-err" role="alert">{{ $t('keyCities.loadFailed', { detail: loadErr }) }}</div>
|
||||
<div v-else-if="!table" class="ct-load">{{ $t('keyCities.tables.loading', { name: $t('keyCities.tables.tabs.' + current.id) }) }}</div>
|
||||
<DataTable
|
||||
v-else
|
||||
|
|
|
|||
Loading…
Reference in a new issue