mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-08-17 21:25:47 +02:00
Implement safe storage utility and update i18n handling to use it
This commit is contained in:
+13
-3
@@ -1,5 +1,8 @@
|
||||
import i18next from 'i18next';
|
||||
import HttpBackend from 'i18next-http-backend';
|
||||
import { getStoredItem, setStoredItem } from '../utils/safe-storage.js';
|
||||
import enCommon from '../../../public/locales/en/common.json';
|
||||
import enTools from '../../../public/locales/en/tools.json';
|
||||
|
||||
// Supported languages
|
||||
export const supportedLanguages = [
|
||||
@@ -73,7 +76,7 @@ export const getLanguageFromUrl = (): SupportedLanguage => {
|
||||
return langMatch[1] as SupportedLanguage;
|
||||
}
|
||||
|
||||
const storedLang = localStorage.getItem('i18nextLng');
|
||||
const storedLang = getStoredItem('i18nextLng');
|
||||
if (
|
||||
storedLang &&
|
||||
supportedLanguages.includes(storedLang as SupportedLanguage)
|
||||
@@ -110,7 +113,7 @@ export const initI18n = async (): Promise<typeof i18next> => {
|
||||
|
||||
const currentLang = getLanguageFromUrl();
|
||||
|
||||
localStorage.setItem('i18nextLng', currentLang);
|
||||
setStoredItem('i18nextLng', currentLang);
|
||||
|
||||
await i18next.use(HttpBackend).init({
|
||||
lng: currentLang,
|
||||
@@ -119,6 +122,13 @@ export const initI18n = async (): Promise<typeof i18next> => {
|
||||
ns: ['common', 'tools'],
|
||||
defaultNS: 'common',
|
||||
preload: [currentLang],
|
||||
partialBundledLanguages: true,
|
||||
resources: {
|
||||
en: {
|
||||
common: enCommon,
|
||||
tools: enTools,
|
||||
},
|
||||
},
|
||||
backend: {
|
||||
loadPath: `${import.meta.env.BASE_URL.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`,
|
||||
},
|
||||
@@ -139,7 +149,7 @@ export const t = (key: string, options?: Record<string, unknown>): string => {
|
||||
|
||||
export const changeLanguage = (lang: SupportedLanguage): void => {
|
||||
if (!supportedLanguages.includes(lang)) return;
|
||||
localStorage.setItem('i18nextLng', lang);
|
||||
setStoredItem('i18nextLng', lang);
|
||||
|
||||
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
let relativePath = window.location.pathname;
|
||||
|
||||
+12
-10
@@ -22,6 +22,11 @@ import {
|
||||
isToolDisabled,
|
||||
isCurrentPageDisabled,
|
||||
} from './utils/disabled-tools.js';
|
||||
import {
|
||||
getStoredItem,
|
||||
setStoredItem,
|
||||
removeStoredItem,
|
||||
} from './utils/safe-storage.js';
|
||||
declare const __BRAND_NAME__: string;
|
||||
|
||||
const init = async () => {
|
||||
@@ -290,17 +295,14 @@ const init = async () => {
|
||||
|
||||
let collapsedCategories: string[] = [];
|
||||
try {
|
||||
const stored = localStorage.getItem('collapsedCategories');
|
||||
const stored = getStoredItem('collapsedCategories');
|
||||
if (stored) collapsedCategories = JSON.parse(stored);
|
||||
} catch {
|
||||
localStorage.removeItem('collapsedCategories');
|
||||
removeStoredItem('collapsedCategories');
|
||||
}
|
||||
|
||||
function saveCollapsedCategories() {
|
||||
localStorage.setItem(
|
||||
'collapsedCategories',
|
||||
JSON.stringify(collapsedCategories)
|
||||
);
|
||||
setStoredItem('collapsedCategories', JSON.stringify(collapsedCategories));
|
||||
}
|
||||
|
||||
const filteredCategories = categories
|
||||
@@ -615,7 +617,7 @@ const init = async () => {
|
||||
) as HTMLInputElement;
|
||||
const toolInterface = document.getElementById('tool-interface');
|
||||
|
||||
const savedFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||
const savedFullWidth = getStoredItem('fullWidthMode') !== 'false';
|
||||
if (fullWidthToggle) {
|
||||
fullWidthToggle.checked = savedFullWidth;
|
||||
applyFullWidthMode(savedFullWidth);
|
||||
@@ -652,7 +654,7 @@ const init = async () => {
|
||||
if (fullWidthToggle) {
|
||||
fullWidthToggle.addEventListener('change', (e) => {
|
||||
const enabled = (e.target as HTMLInputElement).checked;
|
||||
localStorage.setItem('fullWidthMode', enabled.toString());
|
||||
setStoredItem('fullWidthMode', enabled.toString());
|
||||
applyFullWidthMode(enabled);
|
||||
});
|
||||
}
|
||||
@@ -661,7 +663,7 @@ const init = async () => {
|
||||
'compact-mode-toggle'
|
||||
) as HTMLInputElement;
|
||||
|
||||
const savedCompactMode = localStorage.getItem('compactMode') === 'true';
|
||||
const savedCompactMode = getStoredItem('compactMode') === 'true';
|
||||
if (compactModeToggle) {
|
||||
compactModeToggle.checked = savedCompactMode;
|
||||
}
|
||||
@@ -681,7 +683,7 @@ const init = async () => {
|
||||
if (compactModeToggle) {
|
||||
compactModeToggle.addEventListener('change', (e) => {
|
||||
const enabled = (e.target as HTMLInputElement).checked;
|
||||
localStorage.setItem('compactMode', enabled.toString());
|
||||
setStoredItem('compactMode', enabled.toString());
|
||||
applyCompactMode(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export const getStoredItem = (key: string): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const setStoredItem = (key: string, value: string): boolean => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeStoredItem = (key: string): boolean => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const blockStorage = () => {
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
configurable: true,
|
||||
get() {
|
||||
throw new DOMException('The operation is insecure.', 'SecurityError');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const failLocaleFetches = () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.reject(new TypeError('NetworkError')))
|
||||
);
|
||||
};
|
||||
|
||||
describe('i18n resilience on hardened browsers', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('getLanguageFromUrl does not throw when localStorage access is blocked', async () => {
|
||||
blockStorage();
|
||||
const { getLanguageFromUrl } = await import('../js/i18n/i18n');
|
||||
expect(() => getLanguageFromUrl()).not.toThrow();
|
||||
expect(getLanguageFromUrl()).toBe('en');
|
||||
});
|
||||
|
||||
it('initI18n succeeds with blocked storage and failing locale fetches', async () => {
|
||||
blockStorage();
|
||||
failLocaleFetches();
|
||||
const { initI18n, t } = await import('../js/i18n/i18n');
|
||||
await expect(initI18n()).resolves.toBeDefined();
|
||||
expect(t('simpleMode.title')).not.toBe('simpleMode.title');
|
||||
});
|
||||
|
||||
it('t() serves bundled English instead of raw keys when fetches fail', async () => {
|
||||
failLocaleFetches();
|
||||
const { initI18n, t } = await import('../js/i18n/i18n');
|
||||
await initI18n();
|
||||
expect(t('simpleMode.title')).toBe('PDF Tools');
|
||||
expect(t('tools:pdfToTiff.name')).toBe('PDF to TIFF');
|
||||
expect(t('common.error')).not.toBe('common.error');
|
||||
});
|
||||
|
||||
it('changeLanguage does not throw when storage is blocked', async () => {
|
||||
blockStorage();
|
||||
const { changeLanguage } = await import('../js/i18n/i18n');
|
||||
expect(() => changeLanguage('de')).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user