Skip to content

Commit

Permalink
Merge pull request #10396 from DestinyItemManager/lang
Browse files Browse the repository at this point in the history
Better handle language change
  • Loading branch information
bhollis authored May 9, 2024
2 parents c227571 + dd0598f commit c65aa1b
Show file tree
Hide file tree
Showing 14 changed files with 125 additions and 85 deletions.
1 change: 0 additions & 1 deletion config/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,6 @@
"PerkDisplay": "Perk display in item popup",
"PerkList": "List",
"PerkGrid": "Grid",
"ReloadDIM": "Reload DIM to finish switching language",
"ResetToDefault": "Reset",
"ReverseSort": "Toggle forward/reverse sort",
"SetSort": "Sort items by:",
Expand Down
3 changes: 1 addition & 2 deletions src/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ import { StorageBroken, storageTest } from './StorageTest';
import Root from './app/Root';
import setupRateLimiter from './app/bungie-api/rate-limit-config';
import { initGoogleAnalytics } from './app/google';
import { initi18n } from './app/i18n';
import { createLanguageObserver, initi18n } from './app/i18n';
import registerServiceWorker from './app/register-service-worker';
import { safariTouchFix } from './app/safari-touch-fix';
import { createWishlistObserver } from './app/wishlists/observers';
import { observe } from 'app/store/observerMiddleware';
import { createLanguageObserver } from 'app/settings/observers';
infoLog(
'app',
`DIM v${$DIM_VERSION} (${$DIM_FLAVOR}) - Please report any errors to https://www.github.com/DestinyItemManager/DIM/issues`,
Expand Down
6 changes: 3 additions & 3 deletions src/app/destiny1/d1-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ export interface D1ManifestDefinitions extends ManifestDefinitions {
* objet that has a property named after each of the tables listed
* above (defs.TalentGrid, etc.).
*/
export function getDefinitions(): ThunkResult<D1ManifestDefinitions> {
export function getDefinitions(force = false): ThunkResult<D1ManifestDefinitions> {
return async (dispatch, getState) => {
let existingManifest = getState().manifest.d1Manifest;
if (existingManifest) {
if (existingManifest && !force) {
return existingManifest;
}
const db = await dispatch(getManifest());
existingManifest = getState().manifest.d1Manifest;
if (existingManifest) {
if (existingManifest && !force) {
return existingManifest;
}
const defs: ManifestDefinitions & { [table: string]: any } = {
Expand Down
6 changes: 3 additions & 3 deletions src/app/destiny2/d2-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,15 @@ export interface D2ManifestDefinitions extends ManifestDefinitions {
* object that has a property named after each of the tables listed
* above (defs.TalentGrid, etc.).
*/
export function getDefinitions(): ThunkResult<D2ManifestDefinitions> {
export function getDefinitions(force = false): ThunkResult<D2ManifestDefinitions> {
return async (dispatch, getState) => {
let existingManifest = d2ManifestSelector(getState());
if (existingManifest) {
if (existingManifest && !force) {
return existingManifest;
}
const db = await dispatch(getManifest(allTables));
existingManifest = d2ManifestSelector(getState());
if (existingManifest) {
if (existingManifest && !force) {
return existingManifest;
}

Expand Down
47 changes: 41 additions & 6 deletions src/app/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { setTag } from '@sentry/browser';
import i18next from 'i18next';
import HttpApi, { HttpBackendOptions } from 'i18next-http-backend';
import de from 'locale/de.json';
Expand All @@ -13,8 +14,11 @@ import ptBR from 'locale/ptBR.json';
import ru from 'locale/ru.json';
import zhCHS from 'locale/zhCHS.json';
import zhCHT from 'locale/zhCHT.json';
import _ from 'lodash';
import enSrc from '../../config/i18n.json';
import { languageSelector } from './dim-api/selectors';
import { humanBytes } from './storage/human-bytes';
import { StoreObserver } from './store/observerMiddleware';
import { infoLog } from './utils/log';

export const DIM_LANG_INFOS = {
Expand Down Expand Up @@ -43,6 +47,10 @@ export const browserLangToDimLang: Record<string, DimLanguage> = {
'zh-Hant': 'zh-cht',
};

export const dimLangToBrowserLang = _.invert(browserLangToDimLang) as Partial<
Record<DimLanguage, string>
>;

// Hot-reload translations in dev. You'll still need to get things to re-render when
// translations change (unless we someday switch to react-i18next)
if (module.hot) {
Expand All @@ -53,12 +61,7 @@ if (module.hot) {
});
}

// Try to pick a nice default language
export function defaultLanguage(): DimLanguage {
const storedLanguage = localStorage.getItem('dimLanguage') as DimLanguage;
if (storedLanguage && DIM_LANGS.includes(storedLanguage)) {
return storedLanguage;
}
function browserLanguage(): DimLanguage {
const currentBrowserLang = window.navigator.language || 'en';
const overriddenLang = Object.entries(browserLangToDimLang).find(([browserLang]) =>
currentBrowserLang.startsWith(browserLang),
Expand All @@ -69,6 +72,15 @@ export function defaultLanguage(): DimLanguage {
return DIM_LANGS.find((lang) => currentBrowserLang.toLowerCase().startsWith(lang)) || 'en';
}

// Try to pick a nice default language
export function defaultLanguage(): DimLanguage {
const storedLanguage = localStorage.getItem('dimLanguage') as DimLanguage;
if (storedLanguage && DIM_LANGS.includes(storedLanguage)) {
return storedLanguage;
}
return browserLanguage();
}

export function initi18n(): Promise<unknown> {
const lang = defaultLanguage();
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -146,3 +158,26 @@ export function initi18n(): Promise<unknown> {
}
});
}

// Reflect the setting changes in stored values and in the DOM
export function createLanguageObserver(): StoreObserver<DimLanguage> {
return {
id: 'i18n-observer',
getObserved: languageSelector,
runInitially: true,
sideEffect: ({ current }) => {
if (current === browserLanguage()) {
localStorage.removeItem('dimLanguage');
} else {
localStorage.setItem('dimLanguage', current);
}
if (current !== i18next.language) {
i18next.changeLanguage(current);
}
setTag('lang', current);
document
.querySelector('html')!
.setAttribute('lang', dimLangToBrowserLang[current] ?? current);
},
};
}
5 changes: 5 additions & 0 deletions src/app/inventory/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export const update = createAction('inventory/UPDATE')<{
currencies: AccountCurrency[];
}>();

/**
* Remove the loaded stores to force them to be recomputed on the next load (used when changing language).
*/
export const clearStores = createAction('inventory/CLEAR_STORES')();

export const profileLoaded = createAction('inventory/PROFILE_LOADED')<{
profile: DestinyProfileResponse;
live: boolean;
Expand Down
6 changes: 6 additions & 0 deletions src/app/inventory/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ export const inventory: Reducer<InventoryState, InventoryAction | AccountsAction
draft.mockProfileData = action.payload;
});

case getType(actions.clearStores):
return {
...state,
stores: [],
};

default:
return state;
}
Expand Down
6 changes: 4 additions & 2 deletions src/app/item-popup/SocketDetailsSelectedPlug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,10 @@ export default function SocketDetailsSelectedPlug({
</div>
<div className={styles.modDescription}>
<h3>
{plug.displayProperties.name}
{plug.hash === socket.emptyPlugItemHash && <> &mdash; {plug.itemTypeDisplayName}</>}
<span>{plug.displayProperties.name}</span>
{plug.hash === socket.emptyPlugItemHash && (
<span> &mdash; {plug.itemTypeDisplayName}</span>
)}
</h3>
{plugDescriptions.perks.map((perkDesc) => (
<React.Fragment key={perkDesc.perkHash}>
Expand Down
2 changes: 1 addition & 1 deletion src/app/loadout-builder/filter/LoadoutOptimizerExotic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ function ChosenExoticOption({
{icon}
<div className={styles.details}>
<div className={styles.title}>{title}</div>
{description}
<div>{description}</div>
</div>
</div>
);
Expand Down
57 changes: 57 additions & 0 deletions src/app/settings/LanguageSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { currentAccountSelector } from 'app/accounts/selectors';
import { getDefinitions as getDefinitionsD1 } from 'app/destiny1/d1-definitions';
import { getDefinitions } from 'app/destiny2/d2-definitions';
import { settingsSelector } from 'app/dim-api/selectors';
import { t } from 'app/i18next-t';
import { clearStores } from 'app/inventory/actions';
import { useThunkDispatch } from 'app/store/thunk-dispatch';
import i18next from 'i18next';
import React from 'react';
import { useSelector } from 'react-redux';
import Select, { mapToOptions } from './Select';
import { useSetSetting } from './hooks';

const languageOptions = mapToOptions({
de: 'Deutsch',
en: 'English',
es: 'Español (España)',
'es-mx': 'Español (México)',
fr: 'Français',
it: 'Italiano',
ko: '한국어',
pl: 'Polski',
'pt-br': 'Português (Brasil)',
ru: 'Русский',
ja: '日本語',
'zh-cht': '繁體中文', // Chinese (Traditional)
'zh-chs': '简体中文', // Chinese (Simplified)
});

export default function LanguageSetting() {
const dispatch = useThunkDispatch();
const settings = useSelector(settingsSelector);
const currentAccount = useSelector(currentAccountSelector);
const setSetting = useSetSetting();

const changeLanguage = async (e: React.ChangeEvent<HTMLSelectElement>) => {
const language = e.target.value;
await i18next.changeLanguage(language);
setSetting('language', language);
if (currentAccount?.destinyVersion === 2) {
await dispatch(getDefinitions(true));
} else if (currentAccount?.destinyVersion === 1) {
await dispatch(getDefinitionsD1(false));
}
dispatch(clearStores());
};

return (
<Select
label={t('Settings.Language')}
name="language"
value={settings.language}
options={languageOptions}
onChange={changeLanguage}
/>
);
}
53 changes: 3 additions & 50 deletions src/app/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ import StreamDeckSettings from 'app/stream-deck/StreamDeckSettings/StreamDeckSet
import { clearAppBadge } from 'app/utils/app-badge';
import { usePageTitle } from 'app/utils/hooks';
import { errorLog } from 'app/utils/log';
import i18next from 'i18next';
import _ from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import ErrorBoundary from '../dim-ui/ErrorBoundary';
import InventoryItem from '../inventory/InventoryItem';
import { AppIcon, faGrid, faList, lockIcon, refreshIcon, unlockedIcon } from '../shell/icons';
import { AppIcon, faGrid, faList, lockIcon, unlockedIcon } from '../shell/icons';
import CharacterOrderEditor from './CharacterOrderEditor';
import Checkbox from './Checkbox';
import { CustomStatsSettings } from './CustomStatsSettings';
import LanguageSetting from './LanguageSetting';
import Select, { mapToOptions } from './Select';
import styles from './SettingsPage.m.scss';
import SortOrderEditor, { SortProperty } from './SortOrderEditor';
Expand All @@ -42,25 +42,6 @@ export const settingClass = styles.setting;
export const fineprintClass = styles.fineprint;
export const horizontalClass = styles.horizontal;

const languageOptions = mapToOptions({
de: 'Deutsch',
en: 'English',
es: 'Español (España)',
'es-mx': 'Español (México)',
fr: 'Français',
it: 'Italiano',
ko: '한국어',
pl: 'Polski',
'pt-br': 'Português (Brasil)',
ru: 'Русский',
ja: '日本語',
'zh-cht': '繁體中文', // Chinese (Traditional)
'zh-chs': '简体中文', // Chinese (Simplified)
});

// This state is outside the settings page because the settings loses its
let languageChanged = false;

const themeOptions = mapToOptions({
default: 'Default (Beyond Light)',
classic: 'DIM Classic',
Expand Down Expand Up @@ -130,15 +111,6 @@ export default function SettingsPage() {
onCheckChange(checked, name);
};

const changeLanguage = (e: React.ChangeEvent<HTMLSelectElement>) => {
languageChanged = true;
const language = e.target.value;
localStorage.setItem('dimLanguage', language);
i18next.changeLanguage(language, () => {
setSetting('language', language);
});
};

const changeTheme = (e: React.ChangeEvent<HTMLSelectElement>) => {
const theme = e.target.value;
setSetting('theme', theme);
Expand All @@ -154,12 +126,6 @@ export default function SettingsPage() {
return false;
};

const reloadDim = (e: React.MouseEvent) => {
e.preventDefault();
window.location.reload();
return false;
};

const changeVaultWeaponGrouping = (e: React.ChangeEvent<HTMLSelectElement>) => {
const vaultWeaponGrouping = e.target.value;
setSetting('vaultWeaponGrouping', vaultWeaponGrouping);
Expand Down Expand Up @@ -278,20 +244,7 @@ export default function SettingsPage() {
<section id="general">
<h2>{t('Settings.Language')}</h2>
<div className={styles.setting}>
<Select
label={t('Settings.Language')}
name="language"
value={settings.language}
options={languageOptions}
onChange={changeLanguage}
/>
{languageChanged && (
<div>
<button type="button" className="dim-button" onClick={reloadDim}>
<AppIcon icon={refreshIcon} /> <span>{t('Settings.ReloadDIM')}</span>
</button>
</div>
)}
<LanguageSetting />
</div>
</section>

Expand Down
15 changes: 0 additions & 15 deletions src/app/settings/observers.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="">
<html lang="en">
<!-- DIM v<%= version %>, built <%= date %> -->

<head>
Expand Down
1 change: 0 additions & 1 deletion src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,6 @@
"PerkDisplay": "Perk display in item popup",
"PerkGrid": "Grid",
"PerkList": "List",
"ReloadDIM": "Reload DIM to finish switching language",
"ResetToDefault": "Reset",
"ReverseSort": "Toggle forward/reverse sort",
"SetSort": "Sort items by:",
Expand Down

0 comments on commit c65aa1b

Please sign in to comment.