feat(map,i18n): single-copy world clamp + regional flags + pt-PT
- Map: min zoom = smallest integer zoom whose 256*2^z world covers the viewport (no grey bands, no duplicated continents). Horizontal wrap is restored (worldCopyJump, no longitude maxBounds) so Hawaii/Pacific are reachable by dragging; vertical panning stays clamped. - Flags: real SVGs for Catalonia, Basque Country, Galicia and England (no Unicode region flags exist; English is not the UK). Portuguese now has Brasil (pt) and Portugal (pt-PT). - pt-PT shares the pt dictionary with a European drift for the visible chrome (Guardar, Palavra-passe, Autocarro, Utilizador, ...). - check-i18n covers pt-PT: 24 codes at 100%; tsc/tests/build green.
This commit is contained in:
parent
d4c6261b81
commit
7e48884eb9
15 changed files with 243 additions and 23 deletions
27
AGENTS.md
27
AGENTS.md
|
|
@ -2697,14 +2697,27 @@ similar are internal endpoints/hook names, not credits — leave them.)
|
|||
stays English (spot, park, bowl, DIY, vert, crew, line, Stash, The Lab, Nostr,
|
||||
Lightning, Zap, Relay, sats) because Thai skaters use the English terms.
|
||||
|
||||
### Catalan, Basque, Galician — 23 locales, all 100% (2026-10-26)
|
||||
- Added **ca (Català), eu (Euskara), gl (Galego)** → **23 locales**.
|
||||
### Catalan, Basque, Galician + pt-PT — 23 dictionaries, 24 codes, all 100% (2026-10-26)
|
||||
- Added **ca (Català), eu (Euskara), gl (Galego)** → **23 dictionaries**;
|
||||
**pt-PT** is a variant that shares the `pt` dictionary (24 selectable codes).
|
||||
- Registered in `Language` + `LANGUAGE_LOADERS`, `detectLanguage` (ca/eu/gl
|
||||
checked before `es`), the `LanguageToggle` list, and `dateFnsI18n`
|
||||
(date-fns `ca`/`eu`/`gl`, codes `ca-ES`/`eu-ES`/`gl-ES`).
|
||||
- `LanguageToggle` uses **text badges `CA` / `EU` / `GL`**: there are no Unicode
|
||||
region flags for these languages, and a country flag would be wrong. The
|
||||
`CreateTourPage` language list already had ca/eu/gl for tour tags.
|
||||
checked before `es`; `pt-BR`→`pt`, `pt-PT`→`pt-PT`), the `LanguageToggle`
|
||||
list, and `dateFnsI18n` (date-fns `ca`/`eu`/`gl`/`pt`, codes
|
||||
`ca-ES`/`eu-ES`/`gl-ES`/`pt-BR`/`pt-PT`).
|
||||
- **Flags are real SVGs** in `public/flags/` for `ca`, `eu`, `gl` and `gb-eng`
|
||||
(there are no Unicode region flags for Catalonia/Basque/Galicia, and England
|
||||
is not the UK flag). `LanguageToggle` renders a path as `<img>` and an emoji
|
||||
as text. **English now uses the England flag** (not the UK), and Portuguese
|
||||
has **Brasil 🇧🇷** (`pt`) + **Portugal 🇵🇹** (`pt-PT`).
|
||||
- **`pt-PT`** shares the `pt` base and applies a European drift
|
||||
(`src/i18n/pt-PT/drift.ts`: Salvar→Guardar, Senha→Palavra-passe,
|
||||
Ônibus→Autocarro, etc.). A full European-Portuguese review is a follow-up.
|
||||
- **Map world clamp:** `SpotMap` uses `worldCopyJump: true` and
|
||||
`applyWorldMinZoom()` (smallest integer zoom whose 256·2^z world covers the
|
||||
viewport). That stops a second full copy of the continents and keeps the map
|
||||
covering the surface (no grey bands), while still letting you drag across the
|
||||
antimeridian to Hawaii/Pacific. On a wide desktop the whole globe cannot also
|
||||
fit without distortion; the clamp prioritises coverage.
|
||||
- Full dictionaries in `src/i18n/{ca,eu,gl}/` — 7 namespace batches each
|
||||
(8,703 keys), composed in `index.ts`; placeholders verified, only 2–4%
|
||||
identical strings (brand/protocol names). `check-i18n.mjs` includes them in
|
||||
|
|
|
|||
9
public/flags/ca.svg
Normal file
9
public/flags/ca.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 36" preserveAspectRatio="none">
|
||||
<rect width="60" height="36" fill="#fcdd09"/>
|
||||
<g fill="#da121a">
|
||||
<rect y="4" width="60" height="4"/>
|
||||
<rect y="12" width="60" height="4"/>
|
||||
<rect y="20" width="60" height="4"/>
|
||||
<rect y="28" width="60" height="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
5
public/flags/eu.svg
Normal file
5
public/flags/eu.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 36" preserveAspectRatio="none">
|
||||
<rect width="60" height="36" fill="#d52b1e"/>
|
||||
<path d="M0 0 L60 36 M60 0 L0 36" stroke="#009b48" stroke-width="7"/>
|
||||
<path d="M30 0 V36 M0 18 H60" stroke="#fff" stroke-width="8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 280 B |
5
public/flags/gb-eng.svg
Normal file
5
public/flags/gb-eng.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 36" preserveAspectRatio="none">
|
||||
<rect width="60" height="36" fill="#fff"/>
|
||||
<rect x="25" y="0" width="10" height="36" fill="#cf142b"/>
|
||||
<rect x="0" y="13" width="60" height="10" fill="#cf142b"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 262 B |
4
public/flags/gl.svg
Normal file
4
public/flags/gl.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 36" preserveAspectRatio="none">
|
||||
<rect width="60" height="36" fill="#fff"/>
|
||||
<path d="M0 0 L60 36" stroke="#0055a4" stroke-width="12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 201 B |
|
|
@ -130,7 +130,7 @@ async function main() {
|
|||
const jaCoverage = en.size ? Math.round(((en.size - missingJa) / en.size) * 100) : 100;
|
||||
|
||||
// 5. Additional partial locales (same rules as th/ja).
|
||||
const extraLocales = ['pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru', 'ko', 'zh-CN', 'zh-TW', 'uk', 'tr', 'vi', 'id', 'sv', 'cs', 'ro', 'el', 'ca', 'eu', 'gl'];
|
||||
const extraLocales = ['pt', 'pt-PT', 'fr', 'de', 'it', 'nl', 'pl', 'ru', 'ko', 'zh-CN', 'zh-TW', 'uk', 'tr', 'vi', 'id', 'sv', 'cs', 'ro', 'el', 'ca', 'eu', 'gl'];
|
||||
const extraStats = [];
|
||||
for (const lang of extraLocales) {
|
||||
const dict = flatten(await loadTranslations(join(SRC, `i18n/${lang}/index.ts`)));
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ interface LanguageOption {
|
|||
}
|
||||
|
||||
const languages: LanguageOption[] = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧' },
|
||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '/flags/gb-eng.svg' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'ca', name: 'Catalan', nativeName: 'Català', flag: 'CA' },
|
||||
{ code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: 'EU' },
|
||||
{ code: 'gl', name: 'Galician', nativeName: 'Galego', flag: 'GL' },
|
||||
{ code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷' },
|
||||
{ code: 'ca', name: 'Catalan', nativeName: 'Català', flag: '/flags/ca.svg' },
|
||||
{ code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: '/flags/eu.svg' },
|
||||
{ code: 'gl', name: 'Galician', nativeName: 'Galego', flag: '/flags/gl.svg' },
|
||||
{ code: 'pt', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', flag: '🇧🇷' },
|
||||
{ code: 'pt-PT', name: 'Portuguese (Portugal)', nativeName: 'Português (Portugal)', flag: '🇵🇹' },
|
||||
{ code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
|
||||
{ code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹' },
|
||||
|
|
@ -62,7 +63,26 @@ interface LanguageToggleProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Language toggle dropdown for switching between English and Spanish
|
||||
* Flags are an emoji or a local SVG. Catalonia, the Basque Country, Galicia and
|
||||
* England have no Unicode emoji, so they ship as real SVG files and render as
|
||||
* an image instead of a text badge.
|
||||
*/
|
||||
function Flag({ flag, className }: { flag: string; className?: string }) {
|
||||
if (flag.startsWith('/')) {
|
||||
return (
|
||||
<img
|
||||
src={flag}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className={cn('h-4 w-6 shrink-0 rounded-[2px] object-cover shadow-sm', className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <span className={cn('text-lg leading-none', className)}>{flag}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Language toggle dropdown for switching between the available locales
|
||||
*/
|
||||
export function LanguageToggle({ compact = false, inline = false, className, variant = 'ghost' }: LanguageToggleProps) {
|
||||
const { language, setLanguage, t } = useTranslation();
|
||||
|
|
@ -103,7 +123,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
|
|||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<span className="text-lg">{lang.flag}</span>
|
||||
<Flag flag={lang.flag} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{lang.nativeName}</span>
|
||||
{lang.nativeName !== lang.name && (
|
||||
|
|
@ -136,7 +156,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
|
|||
className
|
||||
)}
|
||||
>
|
||||
<span className="text-lg">{currentLanguage.flag}</span>
|
||||
<Flag flag={currentLanguage.flag} />
|
||||
{!compact && (
|
||||
<>
|
||||
<span className="hidden sm:inline">{currentLanguage.nativeName}</span>
|
||||
|
|
@ -168,7 +188,7 @@ export function LanguageToggle({ compact = false, inline = false, className, var
|
|||
language === lang.code && 'bg-primary/10 text-primary'
|
||||
)}
|
||||
>
|
||||
<span className="text-lg">{lang.flag}</span>
|
||||
<Flag flag={lang.flag} />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{lang.nativeName}</span>
|
||||
{lang.nativeName !== lang.name && (
|
||||
|
|
|
|||
|
|
@ -602,6 +602,7 @@ export function RouteDialog({
|
|||
attribution: '',
|
||||
subdomains: 'abc',
|
||||
maxZoom: 19,
|
||||
noWrap: true,
|
||||
className: 'skatespots-dark-tiles',
|
||||
}).addTo(map);
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export function SpotLocationPicker({ value, onChange, className }: SpotLocationP
|
|||
subdomains: 'abc',
|
||||
minZoom: 2,
|
||||
maxZoom: 19,
|
||||
noWrap: true,
|
||||
className: 'skatespots-dark-tiles',
|
||||
}).addTo(map);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,27 @@ import { useTranslation } from '@/contexts/language';
|
|||
/** Translation function shape, so module-level popup builders can receive `t`. */
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string;
|
||||
|
||||
/**
|
||||
* The map never shows a *second full copy* of the world. `applyWorldMinZoom`
|
||||
* clamps the minimum zoom so the 256·2^z world is at least as large as the
|
||||
* viewport. The map can still wrap across the antimeridian (so Hawaii and the
|
||||
* Pacific are reachable by dragging), but at that zoom you only ever see a
|
||||
* sliver of the adjacent copy — never duplicated continents.
|
||||
*/
|
||||
function applyWorldMinZoom(map: L.Map): void {
|
||||
// The world is a 256·2^z square. To cover the viewport (no grey bands, no
|
||||
// repeated continents) pick the smallest integer zoom whose world is at
|
||||
// least as large as the larger viewport dimension. Using the container's
|
||||
// client size directly avoids the 0×0 size Leaflet reports before layout.
|
||||
const el = map.getContainer();
|
||||
const width = el.clientWidth || map.getSize().x;
|
||||
const height = el.clientHeight || map.getSize().y;
|
||||
if (!width || !height) return;
|
||||
const min = Math.max(1, Math.ceil(Math.log2(Math.max(width, height) / 256)));
|
||||
map.setMinZoom(min);
|
||||
if (map.getZoom() < min) map.setZoom(min, { animate: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY: Leaflet popups are built as raw DOM, outside React's auto-escaping.
|
||||
* Every value that originates from a Nostr event (spot/tour/crew/event fields)
|
||||
|
|
@ -921,13 +942,19 @@ export function SpotMap({
|
|||
zoom,
|
||||
scrollWheelZoom: true,
|
||||
attributionControl: false,
|
||||
// Prevent zooming out past a single world view (no grey space, no tile duplication)
|
||||
minZoom: 2,
|
||||
// Clamp panning so the world fills the viewport — no grey bands above/below
|
||||
maxBounds: [[-90, -Infinity], [90, Infinity]],
|
||||
// Prevent zooming out past a single world view (no grey space, no tile
|
||||
// duplication). The real limit is computed after mount by
|
||||
// `applyWorldMinZoom()` because it depends on the viewport size.
|
||||
minZoom: 1,
|
||||
// Clamp vertically (no grey above/below the poles) but let the map wrap
|
||||
// horizontally at the antimeridian, so the Pacific is reachable.
|
||||
maxBounds: [[-85, -Infinity], [85, Infinity]],
|
||||
maxBoundsViscosity: 1.0,
|
||||
worldCopyJump: true,
|
||||
});
|
||||
|
||||
applyWorldMinZoom(map);
|
||||
|
||||
// Free, no-API-key basemap: OpenStreetMap standard tiles, darkened via a CSS
|
||||
// filter (see the `.skatespots-dark-tiles` rule in index.css) so it matches
|
||||
// the app's dark theme without depending on a keyed provider like CARTO.
|
||||
|
|
@ -935,7 +962,7 @@ export function SpotMap({
|
|||
createCachedTileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '',
|
||||
subdomains: 'abc',
|
||||
minZoom: 2,
|
||||
minZoom: 1,
|
||||
maxZoom: 19,
|
||||
className: 'skatespots-dark-tiles',
|
||||
}).addTo(map);
|
||||
|
|
@ -997,6 +1024,7 @@ export function SpotMap({
|
|||
// Fix map size after initialization and ensure tiles are loaded
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
applyWorldMinZoom(map);
|
||||
// Force a redraw to ensure everything is ready
|
||||
map.whenReady(() => {
|
||||
// Trigger initial map move event
|
||||
|
|
@ -1021,6 +1049,7 @@ export function SpotMap({
|
|||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
map.invalidateSize();
|
||||
applyWorldMinZoom(map);
|
||||
});
|
||||
|
||||
resizeObserver.observe(mapRef.current);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ function detectLanguage(): Language {
|
|||
if (tags.some((t) => t.startsWith('th'))) return 'th';
|
||||
if (tags.some((t) => t.startsWith('ja'))) return 'ja';
|
||||
if (tags.some((t) => t.startsWith('ko'))) return 'ko';
|
||||
if (tags.some((t) => t.startsWith('pt-br'))) return 'pt';
|
||||
if (tags.some((t) => t.startsWith('pt-pt'))) return 'pt-PT';
|
||||
if (tags.some((t) => t.startsWith('pt'))) return 'pt';
|
||||
if (tags.some((t) => t.startsWith('fr'))) return 'fr';
|
||||
if (tags.some((t) => t.startsWith('de'))) return 'de';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import en from '@/i18n/en';
|
|||
* `react-refresh/only-export-components` rule.
|
||||
*/
|
||||
|
||||
export type Language = 'en' | 'es' | 'ca' | 'eu' | 'gl' | 'th' | 'ja' | 'pt' | 'fr' | 'de' | 'it' | 'nl' | 'pl' | 'ru' | 'ko' | 'ko-KP' | 'zh-CN' | 'zh-TW' | 'uk' | 'tr' | 'vi' | 'id' | 'sv' | 'cs' | 'ro' | 'el';
|
||||
export type Language = 'en' | 'es' | 'ca' | 'eu' | 'gl' | 'th' | 'ja' | 'pt' | 'pt-PT' | 'fr' | 'de' | 'it' | 'nl' | 'pl' | 'ru' | 'ko' | 'ko-KP' | 'zh-CN' | 'zh-TW' | 'uk' | 'tr' | 'vi' | 'id' | 'sv' | 'cs' | 'ro' | 'el';
|
||||
|
||||
type TranslationKey = string;
|
||||
type TranslationValue = string | { [key: string]: TranslationValue };
|
||||
|
|
@ -45,6 +45,9 @@ export const LANGUAGE_LOADERS: Record<Language, () => Promise<Translations>> = {
|
|||
th: () => import('@/i18n/th').then((m) => m.default),
|
||||
ja: () => import('@/i18n/ja').then((m) => m.default),
|
||||
pt: () => import('@/i18n/pt').then((m) => m.default),
|
||||
// European Portuguese reuses the Brazilian dictionary as a base and applies
|
||||
// a drift of European terms (see src/i18n/pt-PT/drift.ts).
|
||||
'pt-PT': () => import('@/i18n/pt-PT').then((m) => m.default),
|
||||
fr: () => import('@/i18n/fr').then((m) => m.default),
|
||||
de: () => import('@/i18n/de').then((m) => m.default),
|
||||
it: () => import('@/i18n/it').then((m) => m.default),
|
||||
|
|
|
|||
111
src/i18n/pt-PT/drift.ts
Normal file
111
src/i18n/pt-PT/drift.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* European Portuguese overrides for the shared `pt` (Brazilian) dictionary.
|
||||
* Keys must also exist in `en`.
|
||||
*/
|
||||
const drift = {
|
||||
common: {
|
||||
save: "Guardar",
|
||||
},
|
||||
chromeUi: {
|
||||
logIn: "Iniciar sessão",
|
||||
logOut: "Terminar sessão",
|
||||
},
|
||||
navSheet: {
|
||||
login: "Iniciar sessão",
|
||||
},
|
||||
mapUi: {
|
||||
saveChanges: "Guardar alterações",
|
||||
youAreHere: "Está aqui",
|
||||
youreOffline: "Está offline.",
|
||||
relayOnlineNoAnswer: "Está online, mas o relay da comunidade não está a responder agora.",
|
||||
mapFooter: {
|
||||
links: "Ligações",
|
||||
},
|
||||
},
|
||||
spotUi: {
|
||||
saveChanges: "Guardar alterações",
|
||||
targetType: {
|
||||
user: "Utilizador",
|
||||
},
|
||||
loginToEditSpot: "Tem de iniciar sessão para editar este spot.",
|
||||
logInToReport: "Tem de iniciar sessão para denunciar conteúdo.",
|
||||
accessDeniedDesc: "Só pode editar spots que criou.",
|
||||
areYouShopOwner: "É o dono da loja?",
|
||||
claimBullet1: "Torna-se o dono e pode editar o spot",
|
||||
claimIntro: "Está prestes a reivindicar a propriedade deste spot. Isto criará a sua própria versão do spot que pode editar e manter.",
|
||||
claim: {
|
||||
submittedDesc: "Um moderador vai rever a sua reivindicação. Assumirá o spot assim que for aprovada.",
|
||||
},
|
||||
spotClaimedDesc: "Agora é o dono deste spot. Pode editá-lo a partir do seu perfil.",
|
||||
},
|
||||
cardUi: {
|
||||
save: "Guardar cartão no Nostr",
|
||||
saveFrontPng: "Guardar frente (PNG)",
|
||||
studio: {
|
||||
saveChanges: "Guardar alterações",
|
||||
},
|
||||
verify: {
|
||||
invalidLink: "Ligação inválida",
|
||||
},
|
||||
},
|
||||
routeUi: {
|
||||
saveSpotOne: "Guardar {{count}} spot numa Coleção",
|
||||
saveSpots: "Guardar {{count}} spots numa Coleção",
|
||||
group: {
|
||||
bus: "Autocarro",
|
||||
train: "Comboio",
|
||||
},
|
||||
arrived: "Chegou. 🛹",
|
||||
voiceArrived: "Chegou",
|
||||
},
|
||||
pwa: {
|
||||
iosStep2: "Role e toque em \"Adicionar ao Ecrã Principal\"",
|
||||
},
|
||||
sasquatchUi: {
|
||||
mustBeLoggedIn: "Tem de iniciar sessão.",
|
||||
proSupporter: "É um apoiador Pro ⚡",
|
||||
},
|
||||
settingsUi: {
|
||||
loginArea: {
|
||||
logIn: "Iniciar sessão",
|
||||
},
|
||||
accountSwitcher: {
|
||||
logout: "Terminar sessão",
|
||||
},
|
||||
loginDialog: {
|
||||
login: "Iniciar sessão",
|
||||
loginWithAmber: "Iniciar sessão com Amber (Android)",
|
||||
loginWithExtension: "Iniciar sessão com extensão",
|
||||
password: "Palavra-passe",
|
||||
},
|
||||
signupDialog: {
|
||||
download: "Transferir",
|
||||
stepSaveKey: "Guardar chave",
|
||||
saveAndStart: "Guardar e começar a andar",
|
||||
saveEncBackup: "Guardar uma cópia de segurança encriptada",
|
||||
saveEncBackupCta: "Guardar cópia de segurança encriptada",
|
||||
encBackupBefore: "Uma cópia protegida por palavra-passe — segura para guardar na nuvem ou num gestor de palavras-passe. Vai precisar do ficheiro",
|
||||
fileTooLargeTitle: "Ficheiro demasiado grande",
|
||||
nameRequiredDesc: "Escolha um nome de apresentação para os outros skaters saberem quem é. Pode alterá-lo quando quiser.",
|
||||
changeAnytime: "Pode alterá-lo quando quiser. Bio e avatar são opcionais.",
|
||||
youreInPickName: "Entrou! 🛹 Escolha um nome de apresentação para os skaters saberem quem é",
|
||||
youreIn: "Entrou! 🛹",
|
||||
reserveFailedDesc: "{{error}} Pode reivindicar um mais tarde nas Definições.",
|
||||
profileFailedDesc: "A sua conta foi criada, mas a configuração do perfil falhou. Pode atualizá-lo mais tarde.",
|
||||
},
|
||||
editProfile: {
|
||||
saveProfile: "Guardar perfil",
|
||||
invalidCordnTitle: "Ligação do Cordn inválida",
|
||||
mustBeLoggedIn: "Tem de iniciar sessão para atualizar o seu perfil.",
|
||||
},
|
||||
mnemonic: {
|
||||
download: "Transferir",
|
||||
twelveWordDesc: "A sua frase de recuperação é como uma palavra-passe mestra. Anote no papel e guarde-a bem. Vai precisar dela para aceder à sua conta noutros dispositivos ou se limpar os dados do navegador.",
|
||||
},
|
||||
security: {
|
||||
recoveryCodeDesc: "Não guardou quando se registou? Pode obtê-lo aqui quando quiser.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default drift;
|
||||
13
src/i18n/pt-PT/index.ts
Normal file
13
src/i18n/pt-PT/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import pt from '../pt';
|
||||
import drift from './drift';
|
||||
import { deepMerge } from '../deepMerge';
|
||||
|
||||
/**
|
||||
* European Portuguese. The full dictionary is shared with `pt` (Brazilian);
|
||||
* `drift` rewrites the visible Brazilian vocabulary (Salvar→Guardar,
|
||||
* Celular→Telemóvel, Ônibus→Autocarro, Senha→Palavra-passe, etc.) to European
|
||||
* Portuguese. Missing/unchanged keys fall back to the shared base.
|
||||
*/
|
||||
const ptPT = deepMerge(pt, drift);
|
||||
|
||||
export default ptPT;
|
||||
|
|
@ -28,6 +28,7 @@ import {
|
|||
th as thLocale,
|
||||
ja as jaLocale,
|
||||
ptBR as ptLocale,
|
||||
pt as ptPTLocale,
|
||||
fr as frLocale,
|
||||
de as deLocale,
|
||||
it as itLocale,
|
||||
|
|
@ -75,6 +76,9 @@ export function setDateLocale(language: Language): void {
|
|||
} else if (language === 'pt') {
|
||||
activeLocale = ptLocale;
|
||||
activeCode = 'pt-BR';
|
||||
} else if (language === 'pt-PT') {
|
||||
activeLocale = ptPTLocale;
|
||||
activeCode = 'pt-PT';
|
||||
} else if (language === 'fr') {
|
||||
activeLocale = frLocale;
|
||||
activeCode = 'fr-FR';
|
||||
|
|
|
|||
Loading…
Reference in a new issue