mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 12:58:35 +03:00
Add LanguageSwitch component for changing app's language (#25)
Co-authored-by: msramalho <19508417+msramalho@users.noreply.github.com>
This commit is contained in:
@@ -37,6 +37,8 @@ Please check our [issues page](https://github.com/bellingcat/ukraine-timemap/iss
|
||||
* `XXXX_EXT` - points to the respective JSONs of the data, for events, sources, and associations
|
||||
* `MAPBOX_TOKEN` - used to load the custom styles
|
||||
* `DATE_FMT` and `TIME_FMT` - how to consume the events' date/time from the API
|
||||
* `store.app.language` - configures default language
|
||||
* `store.app.languages` - configures available languages
|
||||
* `store.app.map` - configures the initial map view and the UX limits
|
||||
* `store.app.cluster` - configures how clusters/bubbles are grouped into larger clusters, larger `radius` means bigger cluster bubbles
|
||||
* `store.app.timeline` - configure timeline ranges, zoom level options, and default range
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const one_day = 1440;
|
||||
module.exports = {
|
||||
title: "ukraine",
|
||||
page_title: {
|
||||
en: "Civilian Harm in Ukraine",
|
||||
ru: "Ущерб гражданскому населению Украины",
|
||||
},
|
||||
display_title: "Civilian Harm in Ukraine",
|
||||
SERVER_ROOT: "https://ukraine.bellingcat.com/ukraine-server",
|
||||
EVENTS_EXT: "/api/ukraine/export_events/deeprows",
|
||||
@@ -63,7 +67,6 @@ module.exports = {
|
||||
'This map plots out and highlights incidents that have resulted in potential civilian impact or harm since Russia began its invasion of Ukraine. The incidents detailed have been collected by Bellingcat researchers. Included in the map are instances where civilian areas and infrastructure have been damaged or destroyed, where the presence of civilian injuries are visible and/or there is the presence of immobile civilian bodies. Collection for the incidences contained in this map began on February 24, 2022. Users can explore incidents by date and location. We intend this to be a living project that will continue to be updated as long as the conflict persists. For more detailed information about the entries included in this map, please refer to our methodology and explainer article which can be read <a href="https://www.bellingcat.com/news/2022/03/17/hospitals-bombed-and-apartments-destroyed-mapping-incidents-of-civilian-harm-in-ukraine/" >here</a>. ',
|
||||
"Image left: Vyacheslav Madiyevskyy/Reuters. Image right: Järva Teataja/Scanpix Baltics via Reuters.",
|
||||
],
|
||||
|
||||
flags: { isInfopoup: false, isCover: false },
|
||||
cover: {
|
||||
title: "About and Methodology",
|
||||
|
||||
@@ -93,7 +93,8 @@
|
||||
"warning": "(!) HECHOS CUESTIONADOS"
|
||||
}
|
||||
},
|
||||
"en-US": {
|
||||
"en": {
|
||||
"language_short": "Eng",
|
||||
"tiles": {
|
||||
"default": "Map",
|
||||
"satellite": "Sat"
|
||||
@@ -192,5 +193,11 @@
|
||||
"receiver": "Receiver",
|
||||
"warning": "(!) Highly questioned"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"language_short": "Рус"
|
||||
},
|
||||
"uk": {
|
||||
"language_short": "Укр"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ let { DATE_FMT, TIME_FMT } = process.env;
|
||||
if (!DATE_FMT) DATE_FMT = "MM/DD/YYYY";
|
||||
if (!TIME_FMT) TIME_FMT = "HH:mm";
|
||||
|
||||
export const language = process.env.store.app.language || "en-US";
|
||||
export const language = process.env.store.app.language || "en";
|
||||
|
||||
export function getPathLeaf(path) {
|
||||
const splitPath = path.split("/");
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../common/utilities.js";
|
||||
import { ToolbarButton } from "./controls/atoms/ToolbarButton";
|
||||
import { FullscreenToggle } from "./controls/FullScreenToggle";
|
||||
import { LanguageSwitch } from "./controls/LanguageSwitch";
|
||||
|
||||
class Toolbar extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -274,6 +275,15 @@ class Toolbar extends React.Component {
|
||||
<div className="toolbar-header" onClick={this.props.methods.onTitle}>
|
||||
<p>{title}</p>
|
||||
</div>
|
||||
<div className="toolbar-languages">
|
||||
<LanguageSwitch
|
||||
language={this.props.language}
|
||||
languages={this.props.languages}
|
||||
actions={{
|
||||
toggleLanguage: this.props.actions.toggleLanguage,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="toolbar-tabs">
|
||||
<TabList>
|
||||
{narrativesExist
|
||||
@@ -347,6 +357,7 @@ function mapStateToProps(state) {
|
||||
narratives: selectors.selectNarratives(state),
|
||||
shapes: selectors.getShapes(state),
|
||||
language: state.app.language,
|
||||
languages: state.app.languages,
|
||||
toolbarCopy: state.app.toolbar,
|
||||
activeFilters: selectors.getActiveFilters(state),
|
||||
activeCategories: selectors.getActiveCategories(state),
|
||||
|
||||
@@ -84,7 +84,7 @@ export const Card = ({
|
||||
onSelect = () => {},
|
||||
sources = [],
|
||||
isSelected = false,
|
||||
language = "en-US",
|
||||
language = "en",
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggle = () => setIsOpen(!isOpen);
|
||||
|
||||
24
src/components/controls/LanguageSwitch.js
Normal file
24
src/components/controls/LanguageSwitch.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createElement } from "react";
|
||||
import copy from "../../common/data/copy.json";
|
||||
|
||||
export function LanguageSwitch({
|
||||
language: currentLanguage,
|
||||
languages,
|
||||
actions: { toggleLanguage },
|
||||
}) {
|
||||
if (!languages || languages.length <= 1) return null;
|
||||
return createElement("div", {
|
||||
className: "language-switch",
|
||||
onClick: () => toggleLanguage(),
|
||||
children: languages.map((language) =>
|
||||
createElement("span", {
|
||||
key: language,
|
||||
className:
|
||||
language !== currentLanguage
|
||||
? "language-option"
|
||||
: "language-option selected",
|
||||
children: copy[language].language_short,
|
||||
})
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,14 @@ import ReactDOM from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./store";
|
||||
import App from "./components/App";
|
||||
import copy from "./common/data/copy.json";
|
||||
|
||||
// XXX: Hack to make migration from "copy.json" and
|
||||
// adding missing translation strings smoother.
|
||||
Object.assign(copy, {
|
||||
ru: { ...copy["en"], ...copy["uk"], ...copy["ru"] },
|
||||
uk: { ...copy["en"], ...copy["ru"], ...copy["uk"] },
|
||||
});
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("explore-app"));
|
||||
root.render(
|
||||
@@ -11,6 +19,21 @@ root.render(
|
||||
</Provider>
|
||||
);
|
||||
|
||||
store.subscribe(() => {
|
||||
const { app } = store.getState();
|
||||
renderAppLanguage(app);
|
||||
});
|
||||
|
||||
// Update language in places that are out of the App's reach
|
||||
function renderAppLanguage({ language }) {
|
||||
const html = document.documentElement;
|
||||
if (language && language !== html.lang) {
|
||||
html.lang = language;
|
||||
const title = process.env.page_title[language];
|
||||
if (title) document.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
// Expressions from https://exceptionshub.com/how-to-detect-safari-chrome-ie-firefox-and-opera-browser.html
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
@@ -227,10 +227,15 @@ function updateDimensions(appState, action) {
|
||||
}
|
||||
|
||||
function toggleLanguage(appState, action) {
|
||||
const otherLanguage = appState.language === "es-MX" ? "en-US" : "es-MX";
|
||||
return Object.assign({}, appState, {
|
||||
language: action.language || otherLanguage,
|
||||
});
|
||||
return {
|
||||
...appState,
|
||||
language: action.language || selectNextLanguage(appState),
|
||||
};
|
||||
function selectNextLanguage({ language, languages }) {
|
||||
const currentIndex = appState.languages.indexOf(language);
|
||||
const nextIndex = (currentIndex + 1) % languages.length;
|
||||
return languages[nextIndex];
|
||||
}
|
||||
}
|
||||
|
||||
function updateSource(appState, action) {
|
||||
|
||||
22
src/scss/languageswitch.scss
Normal file
22
src/scss/languageswitch.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.language-switch {
|
||||
padding: 1.5em 1em;
|
||||
color: $midwhite;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
.language-option {
|
||||
padding: 0.5em 0.25em;
|
||||
transition: 0.2s ease;
|
||||
border-bottom: solid 2px transparent;
|
||||
&.selected {
|
||||
font-weight: bold;
|
||||
border-bottom-color: $midwhite;
|
||||
color: $offwhite;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.language-option {
|
||||
border-bottom-color: $offwhite;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,4 @@
|
||||
@import "cover";
|
||||
@import "search";
|
||||
@import "satelliteoverlaytoggle";
|
||||
@import "languageswitch";
|
||||
|
||||
@@ -66,7 +66,8 @@ const initial = {
|
||||
},
|
||||
},
|
||||
shapes: [],
|
||||
language: "en-US",
|
||||
language: "en",
|
||||
languages: ["en", "ru", "uk"],
|
||||
cluster: {
|
||||
radius: 30,
|
||||
minZoom: 2,
|
||||
|
||||
Reference in New Issue
Block a user