fix(ui): stop old translations and page shrink on Android
Build, Test & Push Frontend / quality-check (push) Successful in 14s
Build, Test & Push Frontend / docker (push) Successful in 26s

The translation files get a content version in their URL, so a
browser that cached an old file loads the new one. On touch
screens the scroll lock of menus and selects no longer adds a
right margin: Android Chrome reported one and the whole page
became narrower.
This commit is contained in:
lobo committed 2026-09-26 06:29:57 +02:00
1 parent 6edb53162e
commit 25d2f0d0bd
4 files changed
+49 -1

No files matched your search

+2 -1
View File
@@ -9,7 +9,8 @@ i18n.use(HttpBackend)
lng: 'de', lng: 'de',
fallbackLng: 'en', fallbackLng: 'en',
backend: { backend: {
loadPath: '/i18n/{{lng}}.json', // A browser kept an old file without its Cache-Control and showed raw keys; a new URL per version avoids that.
loadPath: `/i18n/{{lng}}.json?v=${__TRANSLATIONS_VERSION__}`,
}, },
interpolation: { interpolation: {
escapeValue: false escapeValue: false
+22
View File
@@ -0,0 +1,22 @@
import {createHash} from 'node:crypto';
import {readdirSync, readFileSync} from 'node:fs';
import path from 'node:path';
import {describe, expect, it, vi} from 'vitest';
vi.mock('i18next-http-backend', () => ({
default: {type: 'backend', init() {}, read(language, namespace, callback) { callback(null, {}); }},
}));
describe('translation files', () => {
it('are loaded under a URL that changes with their content, so no browser keeps an old copy', async () => {
const {default: i18n} = await import('./i18n.js');
const loadPath = i18n.options.backend.loadPath;
const dir = path.resolve(import.meta.dirname, '../public/i18n');
const hash = createHash('sha256');
for (const file of readdirSync(dir).filter((name) => name.endsWith('.json')).sort()) {
hash.update(readFileSync(path.join(dir, file)));
}
expect(loadPath).toBe(`/i18n/{{lng}}.json?v=${hash.digest('hex').slice(0, 12)}`);
});
});
+9
View File
@@ -191,3 +191,12 @@
@apply bg-background text-foreground antialiased; @apply bg-background text-foreground antialiased;
} }
} }
/* Radix menus and selects lock the page scroll and add a right margin as wide as the scrollbar, measured as
innerWidth minus the page width. Android Chrome can report a larger innerWidth, and then the whole page shrinks.
Touch screens have no scrollbar to make room for. */
@media (pointer: coarse) {
html body[data-scroll-locked] {
margin-right: 0 !important;
}
}
+16
View File
@@ -1,13 +1,29 @@
import {createHash} from 'node:crypto';
import {readdirSync, readFileSync} from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
const translationsDir = path.resolve(import.meta.dirname, 'public/i18n');
// Changes whenever a translation changes, so the browser loads the new file under a new URL.
const translationsVersion = () => {
const hash = createHash('sha256');
for (const file of readdirSync(translationsDir).filter((name) => name.endsWith('.json')).sort()) {
hash.update(readFileSync(path.join(translationsDir, file)));
}
return hash.digest('hex').slice(0, 12);
};
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
react(), react(),
tailwindcss(), tailwindcss(),
], ],
define: {
__TRANSLATIONS_VERSION__: JSON.stringify(translationsVersion()),
},
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(import.meta.dirname, './src'), '@': path.resolve(import.meta.dirname, './src'),