Merge branch 'feat/do-perf-guard-2' into feat/desktop-offline

This commit is contained in:
Linus Rath
2026-11-03 21:50:37 +01:00
3 changed files with 54 additions and 5 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ export function DesktopHost() {
// Menus and spellcheck follow the interface.
useEffect(() => {
const ui = uiLocale || document.documentElement.lang;
if (ui) setDesktopInterfaceLanguage(ui, shellLabels(tRef.current as unknown as (key: string) => string));
if (ui) setDesktopInterfaceLanguage(ui, shellLabels(tRef.current as unknown as Parameters<typeof shellLabels>[0]));
}, [uiLocale]);
// What the user writes in, whichever composer asks for it too.
@@ -0,0 +1,45 @@
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { createTranslator } from 'use-intl/core';
import { shellLabels } from '../languages';
/**
* The real catalogs through the real formatter, which the vitest next-intl mock isn't.
* A string that needs a value and doesn't get one is an error here. In the app it's a
* logged error, and the desktop's menu would show the key instead of the label.
*/
const LOCALES_DIR = join(process.cwd(), 'locales');
const LOCALES = readdirSync(LOCALES_DIR, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
function labelsFor(locale: string): { labels: Record<string, string>; errors: string[] } {
const messages = JSON.parse(readFileSync(join(LOCALES_DIR, locale, 'common.json'), 'utf8'));
const errors: string[] = [];
const t = createTranslator({
locale,
messages,
onError: (error) => { errors.push(`${locale}: ${error.message.slice(0, 160)}`); },
});
return { labels: shellLabels(t as unknown as Parameters<typeof shellLabels>[0]), errors };
}
describe('the labels the desktop shell gets', () => {
it('finds every language', () => {
expect(LOCALES.length).toBeGreaterThanOrEqual(30);
});
it.each(LOCALES)('%s: every string formats, with no value missing', (locale) => {
const { labels, errors } = labelsFor(locale);
expect(errors).toEqual([]);
for (const [name, label] of Object.entries(labels)) {
expect(label, name).not.toMatch(/^[\w-]+(\.[\w-]+)+$/);
expect(label.trim(), name).not.toBe('');
}
});
it.each(LOCALES)('%s: leaves the shell its placeholders to fill in', (locale) => {
const { labels } = labelsFor(locale);
expect(labels.contextAddToDictionary).toContain('{word}');
expect(labels.unsentMany).toContain('{count}');
});
});
+8 -4
View File
@@ -33,8 +33,12 @@ export function noteComposerLanguage(key: string, language: string | null): void
push();
}
/** The shell's text in the interface language, by its keys in src/main/strings.ts. */
export function shellLabels(t: (key: string) => string): Record<string, string> {
/**
* The shell's text in the interface language, by its keys in src/main/strings.ts. The
* shell fills in `{word}` and `{count}` itself when it shows them, so they go over as
* they are. Without a value next-intl gives back the key, and that would be the label.
*/
export function shellLabels(t: (key: string, values?: Record<string, string>) => string): Record<string, string> {
return {
menuFile: t('desktopUi.menu.file'),
menuEdit: t('desktopUi.menu.edit'),
@@ -99,7 +103,7 @@ export function shellLabels(t: (key: string) => string): Record<string, string>
contextCopy: t('desktopUi.menu.copy'),
contextPaste: t('desktopUi.menu.paste'),
contextSelectAll: t('desktopUi.menu.select_all'),
contextAddToDictionary: t('desktopUi.context.add_to_dictionary'),
contextAddToDictionary: t('desktopUi.context.add_to_dictionary', { word: '{word}' }),
contextNoSuggestions: t('desktopUi.context.no_suggestions'),
contextOpenLink: t('desktopUi.context.open_link'),
contextCopyLink: t('desktopUi.context.copy_link'),
@@ -111,7 +115,7 @@ export function shellLabels(t: (key: string) => string): Record<string, string>
closeAnyway: t('desktopUi.closing.close_anyway'),
unsentTitle: t('desktopUi.closing.unsent_title'),
unsentOne: t('desktopUi.closing.unsent_one'),
unsentMany: t('desktopUi.closing.unsent_many'),
unsentMany: t('desktopUi.closing.unsent_many', { count: '{count}' }),
closeNow: t('desktopUi.closing.quit'),
leaveTitle: t('desktopUi.closing.leave_title'),
leaveDetail: t('desktopUi.closing.leave_detail'),