diff --git a/package.json b/package.json index 0a46dbf0a..e1443d63c 100644 --- a/package.json +++ b/package.json @@ -181,8 +181,8 @@ "@types/node": "^25.9.5", "@types/node-forge": "^1.3.14", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", "@vitejs/plugin-react": "^6.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4053c985d..eabe2a386 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -149,7 +149,7 @@ importers: version: 6.10.0(@testing-library/dom@10.4.1) '@testing-library/react': specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) @@ -169,11 +169,11 @@ importers: specifier: ^1.5.6 version: 1.5.6 '@types/react': - specifier: ^19.2.17 - version: 19.2.17 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) + specifier: ^19.2.4 + version: 19.2.4(@types/react@19.2.18) '@typescript-eslint/eslint-plugin': specifier: ^8.65.0 version: 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@6.0.3) @@ -266,7 +266,7 @@ importers: version: 5.0.0(leaflet@1.9.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) recharts: specifier: ^3.10.1 - version: 3.10.1(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react-is@17.0.2)(react@19.2.8)(redux@5.0.1) + version: 3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@17.0.2)(react@19.2.8)(redux@5.0.1) sort-package-json: specifier: ^3.7.1 version: 3.7.1 @@ -290,7 +290,7 @@ importers: version: 1.0.0-pre.5(vitest@4.1.10) zustand: specifier: ^5.0.14 - version: 5.0.14(@types/react@19.2.17)(immer@11.1.15)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + version: 5.0.14(@types/react@19.2.18)(immer@11.1.15)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) packages: @@ -1344,13 +1344,13 @@ packages: '@types/qrcode@1.5.6': resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/readable-stream@4.0.24': resolution: {integrity: sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==} @@ -2081,8 +2081,8 @@ packages: electron-publish@26.15.3: resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} - electron-to-chromium@1.5.398: - resolution: {integrity: sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==} + electron-to-chromium@1.5.399: + resolution: {integrity: sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==} electron-updater@6.8.9: resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==} @@ -4349,11 +4349,11 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} - tldts-core@7.4.9: - resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} + tldts-core@7.4.10: + resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} - tldts@7.4.9: - resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} + tldts@7.4.10: + resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} hasBin: true tmp-promise@3.0.3: @@ -5335,7 +5335,7 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.8)(redux@5.0.1))(react@19.2.8)': + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8)': dependencies: '@standard-schema/spec': 1.1.0 '@standard-schema/utils': 0.3.0 @@ -5345,7 +5345,7 @@ snapshots: reselect: 5.2.0 optionalDependencies: react: 19.2.8 - react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.8)(redux@5.0.1) + react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1) '@reteps/dockerfmt-darwin-arm64@0.5.4': optional: true @@ -5659,15 +5659,15 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 '@testing-library/dom': 10.4.1 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -5768,11 +5768,11 @@ snapshots: dependencies: '@types/node': 25.9.5 - '@types/react-dom@19.2.3(@types/react@19.2.17)': + '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -6209,7 +6209,7 @@ snapshots: dependencies: baseline-browser-mapping: 2.11.8 caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.398 + electron-to-chromium: 1.5.399 node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.7) @@ -6612,7 +6612,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-to-chromium@1.5.398: {} + electron-to-chromium@1.5.399: {} electron-updater@6.8.9(supports-color@8.1.1): dependencies: @@ -8589,13 +8589,13 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - react-redux@9.3.0(@types/react@19.2.17)(react@19.2.8)(redux@5.0.1): + react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 react: 19.2.8 use-sync-external-store: 1.6.0(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 redux: 5.0.1 react@19.2.8: {} @@ -8649,9 +8649,9 @@ snapshots: process: 0.11.10 string_decoder: 1.3.0 - recharts@3.10.1(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react-is@17.0.2)(react@19.2.8)(redux@5.0.1): + recharts@3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@17.0.2)(react@19.2.8)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.8)(redux@5.0.1))(react@19.2.8) + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.50.0 @@ -8660,7 +8660,7 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) react-is: 17.0.2 - react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.8)(redux@5.0.1) + react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1) reselect: 5.2.0 tiny-invariant: 1.3.3 use-sync-external-store: 1.6.0(react@19.2.8) @@ -9199,11 +9199,11 @@ snapshots: tinyrainbow@3.1.1: {} - tldts-core@7.4.9: {} + tldts-core@7.4.10: {} - tldts@7.4.9: + tldts@7.4.10: dependencies: - tldts-core: 7.4.9 + tldts-core: 7.4.10 tmp-promise@3.0.3: dependencies: @@ -9217,7 +9217,7 @@ snapshots: tough-cookie@6.0.2: dependencies: - tldts: 7.4.9 + tldts: 7.4.10 tr46@6.0.0: dependencies: @@ -9628,9 +9628,9 @@ snapshots: zod@4.4.3: {} - zustand@5.0.14(@types/react@19.2.17)(immer@11.1.15)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): + zustand@5.0.14(@types/react@19.2.18)(immer@11.1.15)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 immer: 11.1.15 react: 19.2.8 use-sync-external-store: 1.6.0(react@19.2.8) diff --git a/scripts/check-flatpak-offline-pnpm.mjs b/scripts/check-flatpak-offline-pnpm.mjs index 1d41fb98a..794646683 100644 --- a/scripts/check-flatpak-offline-pnpm.mjs +++ b/scripts/check-flatpak-offline-pnpm.mjs @@ -23,6 +23,7 @@ import { missingOfflineTarballs, parseGeneratedPnpmManifest, listLockfilePackageIds, + resolveFlatpakNodeGeneratorBin, storeVersionFromPackageManager, } from './flatpakPnpmStoreVersion.mjs'; @@ -39,15 +40,16 @@ const GENERATOR_TIMEOUT_MS = 5 * 60 * 1000; * @returns {string | null} */ function resolveGeneratorBin() { - const fromEnv = process.env.FLATPAK_NODE_GENERATOR?.trim(); - if (fromEnv) return fromEnv; - - const which = spawnSync('which', ['flatpak-node-generator'], { encoding: 'utf8' }); - if (which.status === 0) { - const bin = which.stdout.trim().split('\n')[0]; - if (bin) return bin; - } - return null; + return resolveFlatpakNodeGeneratorBin({ + root: ROOT, + env: process.env, + which: () => { + const result = spawnSync('which', ['flatpak-node-generator'], { encoding: 'utf8' }); + if (result.status !== 0) return null; + const bin = result.stdout.trim().split('\n')[0]; + return bin || null; + }, + }); } /** diff --git a/scripts/flatpakPnpmStoreVersion.mjs b/scripts/flatpakPnpmStoreVersion.mjs index 20157fad0..fc64e6362 100644 --- a/scripts/flatpakPnpmStoreVersion.mjs +++ b/scripts/flatpakPnpmStoreVersion.mjs @@ -2,6 +2,8 @@ * Flatpak offline pnpm store version must match the packageManager major * (pnpm 11 → store v11). flatpak-node-generator defaults to v10 for lockfile 9. */ +import fs from 'node:fs'; +import path from 'node:path'; /** * Pinned flatpak-builder-tools commit used by Flatpak CI + PR offline checks. @@ -30,6 +32,72 @@ export const FLATPAK_NODE_GENERATOR_PIP_INSTALL_ARGS = [ /** Shell one-liner for workflows / docs (keep in sync with PIP_INSTALL_ARGS). */ export const FLATPAK_NODE_GENERATOR_PIP_INSTALL_CMD = `pip3 ${FLATPAK_NODE_GENERATOR_PIP_INSTALL_ARGS.map((a) => (/\s/.test(a) ? `'${a}'` : a)).join(' ')}`; +/** Documented local CI-pin venv (see check:flatpak-offline-pnpm install hint). */ +export const FLATPAK_NODE_GENERATOR_LOCAL_VENV_DIR = '.cache/flatpak-node-venv'; + +/** + * Resolve flatpak-node-generator: FLATPAK_NODE_GENERATOR → PATH → local CI-pin venv. + * + * @param {{ + * root: string; + * env?: NodeJS.ProcessEnv | Record; + * which?: (() => string | null) | null; + * platform?: NodeJS.Platform; + * existsSync?: (p: string) => boolean; + * accessSync?: (p: string, mode?: number) => void; + * X_OK?: number; + * }} opts + * @returns {string | null} + */ +export function resolveFlatpakNodeGeneratorBin(opts) { + const env = opts.env ?? process.env; + const fromEnv = + typeof env.FLATPAK_NODE_GENERATOR === 'string' ? env.FLATPAK_NODE_GENERATOR.trim() : ''; + if (fromEnv) return fromEnv; + + if (opts.which) { + const fromPath = opts.which()?.trim(); + if (fromPath) return fromPath; + } + + const platform = opts.platform ?? process.platform; + const exists = opts.existsSync ?? ((p) => fs.existsSync(p)); + const access = opts.accessSync ?? ((p, mode) => fs.accessSync(p, mode)); + const xOk = opts.X_OK ?? fs.constants.X_OK; + + /** @param {string} candidate */ + const usable = (candidate) => { + try { + if (!exists(candidate)) return false; + access(candidate, xOk); + return true; + } catch { + // catch-no-log-ok missing/non-executable local venv is a normal miss, not a fault + return false; + } + }; + + if (platform === 'win32') { + const winBin = path.join( + opts.root, + FLATPAK_NODE_GENERATOR_LOCAL_VENV_DIR, + 'Scripts', + 'flatpak-node-generator.exe', + ); + return usable(winBin) ? winBin : null; + } + + const unixBin = path.join( + opts.root, + FLATPAK_NODE_GENERATOR_LOCAL_VENV_DIR, + 'bin', + 'flatpak-node-generator', + ); + if (usable(unixBin)) return unixBin; + + return null; +} + /** * @param {string | null | undefined} packageManager * @returns {number | null} diff --git a/scripts/flatpakPnpmStoreVersion.test.mjs b/scripts/flatpakPnpmStoreVersion.test.mjs index 47a59871c..03749f981 100644 --- a/scripts/flatpakPnpmStoreVersion.test.mjs +++ b/scripts/flatpakPnpmStoreVersion.test.mjs @@ -1,8 +1,10 @@ // @vitest-environment node import { describe, expect, it } from 'vitest'; import yaml from 'js-yaml'; +import path from 'node:path'; import { expectedPnpmStoreVersion, + FLATPAK_NODE_GENERATOR_LOCAL_VENV_DIR, flatpakWorkflowGeneratorInstallViolations, flatpakWorkflowStoreVersionViolations, generatedSourcesStoreDirYamlViolations, @@ -13,11 +15,130 @@ import { parseGeneratedPnpmManifest, pnpmMajorFromPackageManager, probePnpmWorkspaceAfterStoreDirAppend, + resolveFlatpakNodeGeneratorBin, storeVersionFromPackageManager, stripNpmrcStoreDirLines, stripPnpmWorkspaceStoreDirLines, } from './flatpakPnpmStoreVersion.mjs'; +describe('resolveFlatpakNodeGeneratorBin', () => { + const root = '/repo'; + const unixVenv = path.join( + root, + FLATPAK_NODE_GENERATOR_LOCAL_VENV_DIR, + 'bin', + 'flatpak-node-generator', + ); + const winVenv = path.join( + root, + FLATPAK_NODE_GENERATOR_LOCAL_VENV_DIR, + 'Scripts', + 'flatpak-node-generator.exe', + ); + + it('prefers FLATPAK_NODE_GENERATOR over PATH and local venv', () => { + expect( + resolveFlatpakNodeGeneratorBin({ + root, + env: { FLATPAK_NODE_GENERATOR: ' /custom/bin ' }, + which: () => '/usr/bin/flatpak-node-generator', + existsSync: () => true, + accessSync: () => {}, + X_OK: 1, + platform: 'linux', + }), + ).toBe('/custom/bin'); + }); + + it('uses PATH when env unset', () => { + expect( + resolveFlatpakNodeGeneratorBin({ + root, + env: {}, + which: () => '/usr/local/bin/flatpak-node-generator', + existsSync: () => true, + accessSync: () => {}, + X_OK: 1, + platform: 'darwin', + }), + ).toBe('/usr/local/bin/flatpak-node-generator'); + }); + + it('falls back to local CI-pin venv when PATH misses', () => { + expect( + resolveFlatpakNodeGeneratorBin({ + root, + env: {}, + which: () => null, + existsSync: (p) => p === unixVenv, + accessSync: () => {}, + X_OK: 1, + platform: 'darwin', + }), + ).toBe(unixVenv); + }); + + it('uses win32 Scripts layout for local venv', () => { + expect( + resolveFlatpakNodeGeneratorBin({ + root, + env: {}, + which: () => null, + existsSync: (p) => p === winVenv, + accessSync: () => {}, + X_OK: 1, + platform: 'win32', + }), + ).toBe(winVenv); + }); + + it('does not fall through to Unix bin on win32 when Scripts exe is missing', () => { + expect( + resolveFlatpakNodeGeneratorBin({ + root, + env: {}, + which: () => null, + existsSync: (p) => p === unixVenv, + accessSync: () => {}, + X_OK: 1, + platform: 'win32', + }), + ).toBeNull(); + }); + + it('returns null when env, PATH, and local venv are all missing', () => { + expect( + resolveFlatpakNodeGeneratorBin({ + root, + env: { FLATPAK_NODE_GENERATOR: ' ' }, + which: () => null, + existsSync: () => false, + accessSync: () => { + throw new Error('not executable'); + }, + X_OK: 1, + platform: 'linux', + }), + ).toBeNull(); + }); + + it('returns null when local venv exists but is not executable', () => { + expect( + resolveFlatpakNodeGeneratorBin({ + root, + env: { FLATPAK_NODE_GENERATOR: ' ' }, + which: () => null, + existsSync: (p) => p === unixVenv, + accessSync: () => { + throw new Error('not executable'); + }, + X_OK: 1, + platform: 'linux', + }), + ).toBeNull(); + }); +}); + describe('flatpakPnpmStoreVersion', () => { it('maps packageManager major to store version', () => { expect(pnpmMajorFromPackageManager('pnpm@11.15.1+sha512.abc')).toBe(11); diff --git a/scripts/i18n-auto-translate-lib.mjs b/scripts/i18n-auto-translate-lib.mjs index 2a28cba97..f800ca281 100644 --- a/scripts/i18n-auto-translate-lib.mjs +++ b/scripts/i18n-auto-translate-lib.mjs @@ -95,10 +95,34 @@ export function setDeepLocaleValue(obj, dotKey, value) { cur[last] = value; } +// Multi-word brands / protocol phrases stripped before the single-token list. +const SKIP_AUDIT_PHRASE_RE = + /\b(Colorado Mesh|Nomad Network|Ripple Networks|mesh-client|Mesh-Client|Liam's|CalTopo|LetsMesh|MeshMapper|Flood Advert|Reticulum)\b/gi; + // Tokens that are legitimately identical across languages and should not be // treated as "untranslated" when a locale value matches English verbatim. const SKIP_AUDIT_RE = - /\b(TAK|Discord|Meshtastic|MeshCore|MQTT|LoRa|GPS|BLE|SNR|RSSI|dBm|Hz|MHz|kHz|bps|ACK|NAK|CSV|JSON|URL|URI|UUID|API|WiFi|USB|Bluetooth|SX126x|GPIO|Base64|base64|AES-128|AES-256|SHA-256|NTP|Hops?|Mesh-Client|MGRS|Firmware|Router)\b/gi; + /\b(TAK|Discord|Meshtastic|MeshCore|MQTT|LoRa|GPS|BLE|SNR|RSSI|dBm|Hz|MHz|kHz|bps|ACK|NAK|CSV|JSON|URL|URI|UUID|API|WiFi|USB|Bluetooth|SX126x|GPIO|Base64|base64|AES-128|AES-256|SHA-256|NTP|Hops?|MGRS|Firmware|Router|Flood|Advert|ADVERT|FLOOD|DIRECT|LOCAL|TXT_MSG|Sniffer|Repeater|Repeaters|Radio|Client|Imperial|hello)\b/gi; + +/** Leaf keys that must stay English (protocol chips / wire samples) — never audit-retranslate. */ +const SKIP_AUDIT_LEAF_KEYS = new Set([ + 'floodAdvertButton', + 'floodAdvertSection', + 'buttonFloodAdvert', + 'sendButtonDm', + 'pinPlaceholder', + 'channelPsksPlaceholder', + 'guestPasswordPlaceholder', + 'filterChipAdvert', + 'filterChipFlood', + 'filterChipDirect', + 'filterChipLocal', + 'filterChipTxtMsg', + 'filterChipGrpTxt', + 'filterChipPath', + 'filterChipTrace', + 'filterChipAnon', +]); /** * Returns true when an English value contains enough non-technical content to @@ -106,9 +130,10 @@ const SKIP_AUDIT_RE = * placeholders and known loanwords/brands are skipped in --audit mode. * @param {string} enVal */ -function hasTranslatableContent(enVal) { +export function hasTranslatableContent(enVal) { const stripped = enVal .replace(/\{\{[^}]+\}\}/g, '') // remove i18next {{placeholders}} + .replace(SKIP_AUDIT_PHRASE_RE, '') .replace(SKIP_AUDIT_RE, '') .replace(/[^a-zA-Z]/g, '') .trim(); @@ -132,6 +157,8 @@ export function filterMissingKeysToTranslate(enKeys, existingFlat, addedEnglishK const { translateAllGaps, hasGitBaseline, auditIdentical = false, enFlat = null } = opts; return enKeys.filter((k) => { if (k in existingFlat) { + const leaf = k.split('.').pop() ?? k; + if (SKIP_AUDIT_LEAF_KEYS.has(leaf)) return false; return ( auditIdentical && enFlat !== null && diff --git a/scripts/i18n-auto-translate-lib.test.mjs b/scripts/i18n-auto-translate-lib.test.mjs index c5f965f1c..5ea6fcb2b 100644 --- a/scripts/i18n-auto-translate-lib.test.mjs +++ b/scripts/i18n-auto-translate-lib.test.mjs @@ -81,8 +81,17 @@ describe('filterMissingKeysToTranslate', () => { }); it('auditIdentical: also includes present keys whose value matches English', () => { - const enFlat = { a: 'Translate me please', b: 'Flood Advert', c: 'Factory Reset' }; - const existing = { a: 'Bereits übersetzt', b: 'Flood Advert' }; // a translated, b same as EN, c absent + const enFlat = { + a: 'Translate me please', + b: 'Flood Advert', + c: 'Factory Reset', + d: 'Keep your node visible on the mesh', + }; + const existing = { + a: 'Bereits übersetzt', + b: 'Flood Advert', + d: 'Keep your node visible on the mesh', + }; // a translated; b protocol phrase (skip); d identical prose (audit); c absent expect( filterMissingKeysToTranslate(Object.keys(enFlat), existing, null, { translateAllGaps: false, @@ -90,7 +99,28 @@ describe('filterMissingKeysToTranslate', () => { auditIdentical: true, enFlat, }), - ).toEqual(['b', 'c']); + ).toEqual(['c', 'd']); + }); + + it('auditIdentical: skips Flood Advert / brand-only / token-only via hasTranslatableContent', () => { + // Generic leaf keys — must not match SKIP_AUDIT_LEAF_KEYS so exclusions come from + // phrase/token stripping in hasTranslatableContent. + const enFlat = { + protocolPhrase: 'Flood Advert', + brandOnly: 'Colorado Mesh', + wirePassword: 'hello', + filterToken: 'ADVERT', + prose: 'Quit mesh-client completely and reconnect', + }; + const existing = { ...enFlat }; + const result = filterMissingKeysToTranslate(Object.keys(enFlat), existing, null, { + translateAllGaps: false, + hasGitBaseline: false, + auditIdentical: true, + enFlat, + }); + // prose still has translatable words after stripping mesh-client + expect(result).toEqual(['prose']); }); it('auditIdentical: skips keys that are genuinely translated (value differs from English)', () => { diff --git a/src/renderer/locales/cs/translation.json b/src/renderer/locales/cs/translation.json index 8ba4e035c..e7ea79534 100644 --- a/src/renderer/locales/cs/translation.json +++ b/src/renderer/locales/cs/translation.json @@ -2002,7 +2002,7 @@ }, "repeaterRpcInProgress": "Žádost o tento opakovač již probíhá", "cliDangerNotConfirmed": "Destruktivní příkazy CLI vyžadují potvrzení na panelu Opakovače.", - "pingFailed": "Ping selhalComment", + "pingFailed": "Ping selhal", "floodScopeBusy": "Potlačení rozsahu zaplavení není dostupné, když probíhá správa opakovače nebo traceroute. Zkuste to znovu při nečinnosti." }, "mqttHints": { @@ -3078,7 +3078,7 @@ "maxHopsFilter": "Max. hops", "maxHopsAll": "Všechny hops", "maxHopsOption": "{{count}} hops", - "resetView": "Přenastavit pohled", + "resetView": "Přenastavit pohled", "compactLeafCount": "{{count}} kompaktní přímé protějšky" }, "peerListPanel": { diff --git a/src/renderer/locales/de/translation.json b/src/renderer/locales/de/translation.json index 62e7fbf77..605651f60 100644 --- a/src/renderer/locales/de/translation.json +++ b/src/renderer/locales/de/translation.json @@ -148,7 +148,7 @@ "historyWindow3d": "3 Tage", "historyWindow7d": "7 Tage", "retentionLimitsHeading": "Aufbewahrung & Grenzen", - "floodAdvertHelp": "Sendet eine Überschwemmungswerbung, wenn sie verbunden ist, und wiederholt sich im gewählten Intervall, um Ihren Knoten im Netz sichtbar zu halten.", + "floodAdvertHelp": "Sendet ein Flood-Advert bei Verbindung und wiederholt es im gewählten Intervall, damit Ihr Knoten im Netz sichtbar bleibt.", "filterDistantNodesCheckbox": "Entfernte Knoten aus Karte und Knotenliste filtern", "maxDistanceLabel": "Abreise:", "latPlaceholderExample": "z.B. 40.12345", @@ -1409,7 +1409,7 @@ "unknownLoraTraffic": "Unbekannter LoRa-Verkehr", "potentialMeshcoreRepeaterConflict": "Potenzieller MeshCore-Repeater-Konflikt", "elevatedNoiseFloor": "Boden mit erhöhtem Geräuschpegel", - "excessiveFlooding": "Übermäßige Überschwemmungen", + "excessiveFlooding": "Übermäßiges Flooding", "reticulumTrafficDetected": "Reticulum-Verkehr erkannt", "highCompanionTxQueue": "High Companion TX-Warteschlange" }, @@ -1978,8 +1978,8 @@ "illegalArg": "Raumbeitrag abgelehnt (ungültiges Nachrichtenformat).", "notFound": "Raumbeitrag abgelehnt (Raumkontakt im Radio nicht gefunden).", "unsupportedCmd": "Raumbeitrag abgelehnt (nicht unterstützter Nachrichtentyp im Radio). Melden Sie sich im Raum an und versuchen Sie es erneut.", - "default": "Zimmerpost vom Radio abgelehnt. Melden Sie sich ab, melden Sie sich erneut an und versuchen Sie es erneut.", - "timeout": "Beim Warten auf das Radio ist die Zeit für den Zimmerposten abgelaufen. Überprüfen Sie die Reichweite oder versuchen Sie es erneut." + "default": "Raumbeitrag vom Radio abgelehnt. Melden Sie sich ab, melden Sie sich erneut an und versuchen Sie es erneut.", + "timeout": "Beim Warten auf das Radio ist die Zeit für den Raumbeitrag abgelaufen. Überprüfen Sie die Reichweite oder versuchen Sie es erneut." }, "roomLogin": { "noRoute": "Keine Route zu diesem Raumserver. Verfolgen Sie den Knoten auf der Karte oder warten Sie auf Pfadanzeigen und versuchen Sie es dann erneut.", @@ -2000,7 +2000,7 @@ }, "repeaterRpcInProgress": "Eine Anfrage für diesen Repeater ist bereits in Bearbeitung", "cliDangerNotConfirmed": "Destruktive CLI-Befehle müssen im Repeater-Panel bestätigt werden.", - "pingFailed": "Ping fehlgeschlagenComment", + "pingFailed": "Ping fehlgeschlagen", "floodScopeBusy": "Das Überschreiben des Flood-Bereichs ist nicht verfügbar, während die Repeater-Administration oder Traceroute ausgeführt wird. Versuchen Sie es im Ruhezustand erneut." }, "mqttHints": { @@ -2334,7 +2334,7 @@ "transportLost": "Serielle oder Bluetooth-Verbindung verloren — Wiederverbindung...", "mqttAddressRequired": "Geben Sie eine MQTT-Brokeradresse ein, bevor Sie das geräteseitige MQTT-Relay aktivieren.", "mqttDefaultServerTls": "Der öffentliche Meshtastic MQTT Broker unterstützt TLS nicht. Deaktivieren Sie TLS oder verwenden Sie eine benutzerdefinierte Broker-Adresse.", - "mqttDefaultServerPort": "Fügen Sie keinen Port in die öffentliche Meshtastic MQTT-Brokeradresse ein (verwenden Sie mqtt.meshtastic.org mit no :port).", + "mqttDefaultServerPort": "Fügen Sie keinen Port in die öffentliche Meshtastic MQTT-Brokeradresse ein (verwenden Sie mqtt.meshtastic.org ohne :port).", "mqttProxyRequired": "Dieses Radio hat kein WLAN. Aktivieren Sie „Proxy zum Client“, damit die App MQTT weiterleitet, oder verwenden Sie LoRa „OK to MQTT“ anstelle von geräteseitigem MQTT.", "serialOverrideRequiresMode": "Die serielle Konsolenüberschreibung ist auf dem Gerät aktiviert. Stellen Sie einen gültigen seriellen Modus am Radio ein, bevor Sie das serielle Modul hier aktivieren." }, @@ -2834,7 +2834,7 @@ "importResultSuccessWithSkipped_other": "{{count}} Kontakte importiert, {{skipped}} übersprungen.", "importResultError": "{{imported}} importiert, {{skipped}} übersprungen. Fehler: {{errors}}", "refreshContacts": "Kontakte aus dem Radio aktualisieren", - "sendFloodAdvert": "Überschwemmungsanzeige senden", + "sendFloodAdvert": "Flood-Advert senden", "filterByContactGroup": "Nach Kontaktgruppe filtern", "manageContactGroups": "Kontaktgruppen verwalten", "manageGroups": "Gruppen verwalten", @@ -3173,7 +3173,7 @@ "label": "Metrik" }, "1": { - "label": "zöllig" + "label": "Imperial" } }, "deviceRoles": { @@ -4412,7 +4412,7 @@ "contacts": "Kontakte", "nodes": "Knoten", "map": "Karte", - "radio": "Auswahlfelder", + "radio": "Radio", "modules": "Module", "telemetry": "Telemetrie", "security": "Sicherheit", @@ -4420,7 +4420,7 @@ "diagnostics": "Diagnostik", "stats": "Statistik", "sniffer": "Sniffer", - "repeaters": "Wiederholbare Felder", + "repeaters": "Repeater", "rf": "RF", "graph": "Grafik", "tak": "TAK", diff --git a/src/renderer/locales/es/translation.json b/src/renderer/locales/es/translation.json index 1b332390b..c031228c7 100644 --- a/src/renderer/locales/es/translation.json +++ b/src/renderer/locales/es/translation.json @@ -148,7 +148,7 @@ "historyWindow3d": "3 dias", "historyWindow7d": "7 dias", "retentionLimitsHeading": "Retención y límites", - "floodAdvertHelp": "Envía un anuncio de inundación cuando está conectado y se repite en el intervalo elegido para mantener su nodo visible en la malla.", + "floodAdvertHelp": "Envía un flood advert cuando está conectado y se repite en el intervalo elegido para mantener su nodo visible en la malla.", "filterDistantNodesCheckbox": "Filtrar nodos distantes del mapa y la lista de nodos", "maxDistanceLabel": "La distancia máxima:", "latPlaceholderExample": "por ejemplo, 40,12345", @@ -459,7 +459,7 @@ "composePlaceholderDm": "DM a {{name}}…", "composePlaceholderConnectFirst": "Conéctate para enviar mensajes", "composePlaceholderMqttOnly": "Escriba un mensaje (a través de MQTT)...", - "composePlaceholderDefault": "Inroducir mensaje aquí", + "composePlaceholderDefault": "Introducir mensaje aquí", "copyMessage": "Copiar mensaje", "filterBySender": "Filtrar por remitente", "clearSenderFilter": "Borrar filtro", @@ -556,7 +556,7 @@ "mentionSuggestionsAria": "Mencionar sugerencias", "openDmByAddress": "Abierto", "dmAddressPlaceholder": "Hash de destino o lxmf://…", - "dmAddressAria": "Dirección de destino del retículo", + "dmAddressAria": "Dirección de destino", "dmAddressInvalid": "Introduzca un hash de destino válido de 32 caracteres o una dirección lxmf://.", "routingErrors": { "pkiMissingRecipientKey": "No se ha podido enviar el mensaje cifrado: falta la clave pública del destinatario de NodeDB.", @@ -938,7 +938,7 @@ "displayNameSaved": "Nombre para mostrar guardado.", "copyLxmfHash": "Copiar hash LXMF", "slotsTitle": "Identidades", - "slotsHint": "Cree identidades adicionales y cambie entre ellas. La conmutación reinicia la pila de retículo.", + "slotsHint": "Cree identidades adicionales y cambie entre ellas. La conmutación reinicia la pila de Reticulum.", "slotActive": "Activo", "slotSwitch": "Cambiar", "slotSwitchAria": "Cambiar a la identidad {{id}}", @@ -946,7 +946,7 @@ "slotDeleteAria": "Eliminar identidad {{id}}", "slotCreate": "Nueva identidad", "slotCreateAria": "Crear una nueva identidad", - "slotSwitchConfirmTitle": "¿Cambiar la identidad del retículo?", + "slotSwitchConfirmTitle": "¿Cambiar la identidad de Reticulum?", "slotSwitchConfirmMessage": "La pila se reiniciará con la identidad seleccionada. Es posible que se interrumpan los mensajes durante el vuelo.", "slotSwitchConfirmAction": "Cambiar identidad", "slotDeleteConfirmTitle": "Borrar la identidad", @@ -1409,7 +1409,7 @@ "unknownLoraTraffic": "Tráfico LoRa desconocido", "potentialMeshcoreRepeaterConflict": "Conflicto potencial del repetidor MeshCore", "elevatedNoiseFloor": "Piso de ruido elevado", - "excessiveFlooding": "Inundaciones excesivas", + "excessiveFlooding": "Flooding excesivo", "reticulumTrafficDetected": "Tráfico Reticulum detectado", "highCompanionTxQueue": "Cola de TX de acompañamiento alto" }, @@ -1603,8 +1603,8 @@ "position": "Posición", "remoteHardware": "Hardware remoto", "admin": "Administración", - "discoveryFlood": "descubrimientoinundación", - "roomAdvert": "Anuncio de habitación", + "discoveryFlood": "DiscoveryFlood", + "roomAdvert": "RoomAdvert", "generic": "Puerto{{port}}" }, "signalColumn": "Señal", @@ -1978,7 +1978,7 @@ "illegalArg": "Publicación de sala rechazada (formato de mensaje no válido).", "notFound": "Publicación de la sala rechazada (el contacto de la sala no se encuentra en la radio).", "unsupportedCmd": "Publicación en sala rechazada (tipo de mensaje no admitido en la radio). Inicie sesión en la sala y vuelva a intentarlo.", - "default": "Publicación de la habitación rechazada por la radio. Cierre sesión, inicie sesión nuevamente y vuelva a intentarlo.", + "default": "Publicación en la sala rechazada por la radio. Cierre sesión, inicie sesión nuevamente y vuelva a intentarlo.", "timeout": "Se agotó el tiempo de espera de la publicación de la sala esperando la radio. Verifique el rango o inténtelo nuevamente." }, "roomLogin": { @@ -2000,7 +2000,7 @@ }, "repeaterRpcInProgress": "Ya hay una solicitud en curso para este repetidor.", "cliDangerNotConfirmed": "Los comandos CLI destructivos requieren confirmación en el panel de repetidores.", - "pingFailed": "Falló la orden pingComment", + "pingFailed": "Falló el ping", "floodScopeBusy": "La anulación del alcance Flood no está disponible mientras la administración del repetidor o el traceroute están en curso. Inténtalo de nuevo cuando esté inactivo." }, "mqttHints": { @@ -2334,7 +2334,7 @@ "transportLost": "Enlace serie o Bluetooth perdido — reconectando...", "mqttAddressRequired": "Introduzca una dirección de bróker MQTT antes de habilitar el relé MQTT del lado del dispositivo.", "mqttDefaultServerTls": "El bróker público Meshtastic MQTT no es compatible con TLS. Desactive TLS o utilice una dirección de bróker personalizada.", - "mqttDefaultServerPort": "No incluya un puerto en la dirección pública del corredor MQTT de Meshtastic (use mqtt.meshtastic.org con no :port).", + "mqttDefaultServerPort": "No incluya un puerto en la dirección pública del broker MQTT de Meshtastic (use mqtt.meshtastic.org sin :port).", "mqttProxyRequired": "Esta radio no tiene wifi. Habilite \"Proxy al cliente\" para que la aplicación reenvíe MQTT, o use LoRa \"OK a MQTT\" en lugar de MQTT del lado del dispositivo.", "serialOverrideRequiresMode": "La anulación de la consola serie está habilitada en el dispositivo. Establezca un modo serie válido en la radio antes de habilitar el módulo serie aquí." }, @@ -2834,7 +2834,7 @@ "importResultSuccessWithSkipped_other": "Se importaron {{count}} contactos, se omitió {{skipped}}.", "importResultError": "Importado {{imported}}, omitido {{skipped}}. Errores: {{errors}}", "refreshContacts": "Actualizar contactos desde la radio", - "sendFloodAdvert": "Enviar anuncio de inundación", + "sendFloodAdvert": "Enviar flood advert", "filterByContactGroup": "Filtrar por grupo de contactos", "manageContactGroups": "Gestionar grupos de contactos", "manageGroups": "Administrar grupos", @@ -4420,7 +4420,7 @@ "app": "Aplicación", "diagnostics": "Diagnóstico", "stats": "Estadísticas", - "sniffer": "detector cromatográfico de explosivos", + "sniffer": "Sniffer", "repeaters": "Repetidoras", "rf": "RF", "graph": "Gráfico", diff --git a/src/renderer/locales/fr/translation.json b/src/renderer/locales/fr/translation.json index 6184c05d7..46fa11c33 100644 --- a/src/renderer/locales/fr/translation.json +++ b/src/renderer/locales/fr/translation.json @@ -148,7 +148,7 @@ "historyWindow3d": "3 jours", "historyWindow7d": "7 jours", "retentionLimitsHeading": "Rétention et limites", - "floodAdvertHelp": "Envoie une annonce d'inondation lorsqu'elle est connectée et se répète à l'intervalle choisi pour garder votre nœud visible sur le maillage.", + "floodAdvertHelp": "Envoie un flood advert lorsqu'elle est connectée et se répète à l'intervalle choisi pour garder votre nœud visible sur le maillage.", "filterDistantNodesCheckbox": "Filtrer les nœuds distants à partir de la carte et de la liste", "maxDistanceLabel": "Distance maxi", "latPlaceholderExample": "par exemple 40.12345", @@ -1409,7 +1409,7 @@ "unknownLoraTraffic": "Trafic LoRa inconnu", "potentialMeshcoreRepeaterConflict": "Conflit potentiel de répéteur MeshCore", "elevatedNoiseFloor": "Plancher de bruit surélevé", - "excessiveFlooding": "Inondations excessives", + "excessiveFlooding": "Flooding excessif", "reticulumTrafficDetected": "Trafic Reticulum détecté", "highCompanionTxQueue": "File d'attente TX Companion élevée" }, @@ -1604,7 +1604,7 @@ "remoteHardware": "Matériel distant", "admin": "Administrateur", "discoveryFlood": "DécouverteInondation", - "roomAdvert": "ChambreAnnonce", + "roomAdvert": "RoomAdvert", "generic": "Port{{port}}" }, "signalColumn": "Signal", @@ -1974,12 +1974,12 @@ "bleTimeoutHandshake": "Bluetooth connecté mais la négociation du protocole MeshCore ne s'est pas terminée avant la déconnexion/l'expiration du délai. Réessayez, gardez l'appareil éveillé et à proximité, redémarrez BLE ou utilisez Serial/TCP.", "bleTimeoutNoble": "La connexion Bluetooth a expiré lors de l'ouverture de MeshCore sur Noble IPC. Réessayez, redémarrez BLE sur l'appareil ou utilisez Serial/TCP.", "roomPost": { - "badState": "Post de chambre rejeté (non connecté à la radio). Déconnectez-vous, reconnectez-vous, puis réessayez.", + "badState": "Publication de salle rejetée (non connecté à la radio). Déconnectez-vous, reconnectez-vous, puis réessayez.", "illegalArg": "Message de salle rejeté (format de message invalide).", - "notFound": "Post de chambre rejeté (contact de chambre introuvable à la radio).", + "notFound": "Publication de salle rejetée (contact de salle introuvable à la radio).", "unsupportedCmd": "Message de salle rejeté (type de message non pris en charge à la radio). Connectez-vous à la salle, puis réessayez.", - "default": "Post de chambre rejeté par la radio. Déconnectez-vous, reconnectez-vous, puis réessayez.", - "timeout": "Le délai de publication dans la chambre a expiré en attendant la radio. Vérifiez la portée ou réessayez." + "default": "Publication de salle rejetée par la radio. Déconnectez-vous, reconnectez-vous, puis réessayez.", + "timeout": "Le délai de publication dans la salle a expiré en attendant la radio. Vérifiez la portée ou réessayez." }, "roomLogin": { "noRoute": "Aucun itinéraire vers ce serveur de salle. Tracez le nœud sur la carte ou attendez les annonces de chemin, puis réessayez.", @@ -1994,13 +1994,13 @@ "failed": "La connexion à la salle a échoué" }, "roomLogout": { - "timedOut": "Le délai de déconnexion de la salle a expiré. La pièce est peut-être hors de portée ou ne répond pas.", + "timedOut": "Le délai de déconnexion de la salle a expiré. La salle est peut-être hors de portée ou ne répond pas.", "rejected": "Déconnexion de la salle refusée par radio.", - "failed": "Impossible de quitter la chambre" + "failed": "Impossible de quitter la salle" }, "repeaterRpcInProgress": "Une demande pour ce répéteur est déjà en cours", "cliDangerNotConfirmed": "Les commandes CLI destructives nécessitent une confirmation dans le panneau Répéteurs.", - "pingFailed": "Échec de la réponseComment", + "pingFailed": "Échec du ping", "floodScopeBusy": "Le remplacement de la portée Flood n'est pas disponible pendant que l'administration du répéteur ou le traceroute est en cours. Réessayez lorsque vous êtes inactif." }, "mqttHints": { @@ -2834,7 +2834,7 @@ "importResultSuccessWithSkipped_other": "Contacts {{count}} importés, {{skipped}} ignorés.", "importResultError": "{{imported}} importé, {{skipped}} ignoré. Erreurs : {{errors}}", "refreshContacts": "Actualiser les contacts de la radio", - "sendFloodAdvert": "Envoyer une annonce d'inondation", + "sendFloodAdvert": "Envoyer un flood advert", "filterByContactGroup": "Filtrer par groupe de contacts", "manageContactGroups": "Gérer les groupes de contacts", "manageGroups": "Gérer les groupes", diff --git a/src/renderer/locales/id/translation.json b/src/renderer/locales/id/translation.json index e8e8c1f20..09d4b5b37 100644 --- a/src/renderer/locales/id/translation.json +++ b/src/renderer/locales/id/translation.json @@ -1978,7 +1978,7 @@ "illegalArg": "Postingan ruangan ditolak (format pesan tidak valid).", "notFound": "Postingan ruangan ditolak (kontak ruangan tidak ditemukan di radio).", "unsupportedCmd": "Postingan ruangan ditolak (jenis pesan di radio tidak didukung). Masuk ke ruangan, lalu coba lagi.", - "default": "Postingan kamar ditolak oleh radio. Keluar, masuk lagi, lalu coba lagi.", + "default": "Postingan ruangan ditolak oleh radio. Keluar, masuk lagi, lalu coba lagi.", "timeout": "Waktu posting kamar habis menunggu radio. Periksa jangkauan atau coba lagi." }, "roomLogin": { @@ -4420,7 +4420,7 @@ "app": "Aplikasi", "diagnostics": "Diagnostik", "stats": "Statistik", - "sniffer": "Sapu tangan", + "sniffer": "Sniffer", "repeaters": "Repeater", "rf": "RF", "graph": "Grafik", diff --git a/src/renderer/locales/it/translation.json b/src/renderer/locales/it/translation.json index 6844b6517..59322e33e 100644 --- a/src/renderer/locales/it/translation.json +++ b/src/renderer/locales/it/translation.json @@ -2334,7 +2334,7 @@ "transportLost": "Collegamento seriale o Bluetooth perso — riconnessione in corso...", "mqttAddressRequired": "Immettere un indirizzo del broker MQTT prima di abilitare il relè MQTT lato dispositivo.", "mqttDefaultServerTls": "Il broker MQTT Meshtastic pubblico non supporta TLS. Disattiva TLS o utilizza un indirizzo broker personalizzato.", - "mqttDefaultServerPort": "Non includere una porta nell'indirizzo pubblico del broker MQTT Meshtastic (utilizzare mqtt.meshtastic.org con no :port).", + "mqttDefaultServerPort": "Non includere una porta nell'indirizzo pubblico del broker MQTT Meshtastic (utilizzare mqtt.meshtastic.org senza :port).", "mqttProxyRequired": "Questa radio non ha Wi-Fi. Abilitare \"Proxy al client\" in modo che l'app inoltri MQTT o utilizzare LoRa \"OK to MQTT\" invece di MQTT sul lato dispositivo.", "serialOverrideRequiresMode": "L'override della console seriale è abilitato sul dispositivo. Impostare una modalità seriale valida sulla radio prima di abilitare il modulo seriale qui." }, @@ -2834,7 +2834,7 @@ "importResultSuccessWithSkipped_other": "Contatti {{count}} importati, ignorati {{skipped}}.", "importResultError": "Importato {{imported}}, saltato {{skipped}}. Errori: {{errors}}", "refreshContacts": "Aggiorna i contatti dalla radio", - "sendFloodAdvert": "Invia annuncio di alluvione", + "sendFloodAdvert": "Invia flood advert", "filterByContactGroup": "Filtra per gruppo di contatti", "manageContactGroups": "Gestisci gruppi di contatti", "manageGroups": "Gestisci gruppi", diff --git a/src/renderer/locales/ja/translation.json b/src/renderer/locales/ja/translation.json index 5819704c9..15ce4a5e5 100644 --- a/src/renderer/locales/ja/translation.json +++ b/src/renderer/locales/ja/translation.json @@ -1978,8 +1978,8 @@ "illegalArg": "ルームの投稿が拒否されました (無効なメッセージ形式)。", "notFound": "ルームの投稿が拒否されました (ルームの連絡先が無線で見つかりません)。", "unsupportedCmd": "ルームの投稿が拒否されました (ラジオでサポートされていないメッセージ タイプ)。ルームにログインしてから再試行してください。", - "default": "部屋の投稿が無線で拒否されました。ログアウトし、再度ログインして、再試行してください。", - "timeout": "ラジオを待っている間に部屋の投稿がタイムアウトになりました。範囲を確認するか、再試行してください。" + "default": "ルームへの投稿が無線で拒否されました。ログアウトし、再度ログインして、再試行してください。", + "timeout": "ラジオを待っている間にルームの投稿がタイムアウトになりました。範囲を確認するか、再試行してください。" }, "roomLogin": { "noRoute": "このルームサーバーへのルートがありません。マップからノードをトレースするか、パスの通知を待ってから再試行してください。", @@ -1994,13 +1994,13 @@ "failed": "ルームへのログインに失敗しました" }, "roomLogout": { - "timedOut": "ルームのログアウトがタイムアウトしました。部屋が圏外か、応答していない可能性があります。", + "timedOut": "ルームのログアウトがタイムアウトしました。ルームが圏外か、応答していない可能性があります。", "rejected": "ルームのログアウトが無線によって拒否されました。", - "failed": "部屋から出られませんでした" + "failed": "ルームから退出できませんでした" }, "repeaterRpcInProgress": "このリピーターに対するリクエストはすでに進行中です", "cliDangerNotConfirmed": "破壊的なCLIコマンドは、リピーターパネルで確認する必要があります。", - "pingFailed": "Ping 失敗Comment", + "pingFailed": "Ping 失敗", "floodScopeBusy": "リピーター管理またはトレースルートの進行中は、フラッド スコープのオーバーライドは使用できません。アイドル状態のときに再試行してください。" }, "mqttHints": { diff --git a/src/renderer/locales/nl/translation.json b/src/renderer/locales/nl/translation.json index 5a33d79c2..d9be70013 100644 --- a/src/renderer/locales/nl/translation.json +++ b/src/renderer/locales/nl/translation.json @@ -2334,7 +2334,7 @@ "transportLost": "Seriële of Bluetooth-link verloren — opnieuw verbinden...", "mqttAddressRequired": "Voer een MQTT-makelaaradres in voordat u het MQTT-relais aan de apparaatzijde inschakelt.", "mqttDefaultServerTls": "De openbare Meshtastic MQTT-makelaar ondersteunt TLS niet. Schakel TLS uit of gebruik een aangepast makelaarsadres.", - "mqttDefaultServerPort": "Neem geen poort op in het openbare Meshtastic MQTT broker-adres (gebruik mqtt.meshtastic.org met no :port).", + "mqttDefaultServerPort": "Neem geen poort op in het openbare Meshtastic MQTT-brokeradres (gebruik mqtt.meshtastic.org zonder :port).", "mqttProxyRequired": "Deze radio heeft geen wifi. Schakel \"Proxy naar client\" in zodat de app MQTT doorstuurt, of gebruik LoRa \"OK naar MQTT\" in plaats van MQTT aan de apparaatzijde.", "serialOverrideRequiresMode": "Seriële console override is ingeschakeld op het apparaat. Stel een geldige seriële modus in op de radio voordat u de seriële module hier inschakelt." }, diff --git a/src/renderer/locales/pl/translation.json b/src/renderer/locales/pl/translation.json index 8d2127e65..48107aa3e 100644 --- a/src/renderer/locales/pl/translation.json +++ b/src/renderer/locales/pl/translation.json @@ -940,7 +940,7 @@ "displayNameSaved": "Nazwa wyświetlana zapisana.", "copyLxmfHash": "Skopiuj skrót LXMF", "slotsTitle": "Tożsamość", - "slotsHint": "Twórz dodatkowe tożsamości i przełączaj się między nimi. Przełączanie restartuje stos Siatkówki.", + "slotsHint": "Twórz dodatkowe tożsamości i przełączaj się między nimi. Przełączanie restartuje stos Reticulum.", "slotActive": "Aktywna", "slotSwitch": "Przełącznik", "slotSwitchAria": "Przełącz na tożsamość {{id}}", @@ -3080,7 +3080,7 @@ "maxHopsFilter": "Maks. hops", "maxHopsAll": "Wszystkie hops", "maxHopsOption": "{{count}} hops", - "resetView": "Odśwież widok", + "resetView": "Resetuj widok", "compactLeafCount": "{{count}} kompaktowych partnerów bezpośrednich" }, "peerListPanel": { diff --git a/src/renderer/locales/pt-BR/translation.json b/src/renderer/locales/pt-BR/translation.json index f95677622..c1602c42a 100644 --- a/src/renderer/locales/pt-BR/translation.json +++ b/src/renderer/locales/pt-BR/translation.json @@ -557,7 +557,7 @@ "mentionSuggestionsAria": "Mencionar sugestões", "openDmByAddress": "Abrir", "dmAddressPlaceholder": "Hash de destino ou lxmf://…", - "dmAddressAria": "Endereço de destino do retículo", + "dmAddressAria": "Endereço de destino", "dmAddressInvalid": "Insira um hash de destino válido de 32 caracteres ou um endereço lxmf://.", "routingErrors": { "pkiMissingRecipientKey": "Não foi possível enviar a mensagem criptografada — a chave pública do destinatário está ausente do NodeDB.", @@ -938,15 +938,15 @@ "displayNameSaved": "Nome de exibição salvo.", "copyLxmfHash": "Copiar hash LXMF", "slotsTitle": "Móveis", - "slotsHint": "Crie identidades adicionais e alterne entre elas. A comutação reinicia a pilha do retículo.", + "slotsHint": "Crie identidades adicionais e alterne entre elas. A comutação reinicia a pilha do Reticulum.", "slotActive": "Ativo", "slotSwitch": "Switch", "slotSwitchAria": "Mudar para identidade {{id}}", "slotDelete": "Excluir", "slotDeleteAria": "Eliminar identidade {{id}}", "slotCreate": "nova identidade", - "slotCreateAria": "Criar uma nova identidade de retículo", - "slotSwitchConfirmTitle": "Mudar a identidade do retículo?", + "slotCreateAria": "Criar uma nova identidade Reticulum", + "slotSwitchConfirmTitle": "Mudar a identidade Reticulum?", "slotSwitchConfirmMessage": "A pilha será reiniciada com a identidade selecionada. As mensagens durante o voo podem ser interrompidas.", "slotSwitchConfirmAction": "Alterar identidade", "slotDeleteConfirmTitle": "Excluir Identidade", @@ -2334,7 +2334,7 @@ "transportLost": "Link serial ou Bluetooth perdido — reconectando...", "mqttAddressRequired": "Insira um endereço de corretor MQTT antes de ativar o relé MQTT do lado do dispositivo.", "mqttDefaultServerTls": "A corretora pública Meshtastic MQTT não suporta TLS. Desative o TLS ou use um endereço de corretor personalizado.", - "mqttDefaultServerPort": "Não inclua uma porta no endereço público do corretor Meshtastic MQTT (use mqtt.meshtastic.org com no :port).", + "mqttDefaultServerPort": "Não inclua uma porta no endereço público do broker MQTT Meshtastic (use mqtt.meshtastic.org sem :port).", "mqttProxyRequired": "Este rádio não tem Wi-Fi. Ative \"Proxy para cliente\" para que o aplicativo encaminhe o MQTT ou use o LoRa \"OK para MQTT\" em vez do MQTT do lado do dispositivo.", "serialOverrideRequiresMode": "A substituição do console serial está ativada no dispositivo. Defina um modo serial válido no rádio antes de ativar o módulo serial aqui." }, diff --git a/src/renderer/locales/ru/translation.json b/src/renderer/locales/ru/translation.json index cc236b989..2ea33da06 100644 --- a/src/renderer/locales/ru/translation.json +++ b/src/renderer/locales/ru/translation.json @@ -2002,7 +2002,7 @@ }, "repeaterRpcInProgress": "Запрос на этот ретранслятор уже обрабатывается", "cliDangerNotConfirmed": "Деструктивные команды CLI требуют подтверждения на панели повторителей.", - "pingFailed": "Comment", + "pingFailed": "Ping не удался", "floodScopeBusy": "Переопределение области флуда недоступно, пока выполняется администрирование ретранслятора или трассировка. Попробуйте еще раз во время простоя." }, "mqttHints": { @@ -2336,7 +2336,7 @@ "transportLost": "Последовательная связь или связь Bluetooth потеряны — повторное подключение...", "mqttAddressRequired": "Введите адрес брокера MQTT перед включением реле MQTT на стороне устройства.", "mqttDefaultServerTls": "Публичный Meshtastic MQTT-брокер не поддерживает TLS. Отключите TLS или используйте пользовательский адрес брокера.", - "mqttDefaultServerPort": "Не включайте порт в публичный адрес брокера Meshtastic MQTT (используйте mqtt.meshtastic.org с no :port).", + "mqttDefaultServerPort": "Не включайте порт в публичный адрес брокера Meshtastic MQTT (используйте mqtt.meshtastic.org без :port).", "mqttProxyRequired": "В этом радио нет Wi-Fi. Включите «Прокси для клиента», чтобы приложение пересылало MQTT, или используйте LoRa «OK to MQTT» вместо MQTT на стороне устройства.", "serialOverrideRequiresMode": "Переопределение последовательной консоли включено на устройстве. Установите допустимый последовательный режим на радиоприемнике, прежде чем включать последовательный модуль здесь." }, diff --git a/src/renderer/locales/tr/translation.json b/src/renderer/locales/tr/translation.json index 09f2bf7f0..1f47cdb0a 100644 --- a/src/renderer/locales/tr/translation.json +++ b/src/renderer/locales/tr/translation.json @@ -2000,7 +2000,7 @@ }, "repeaterRpcInProgress": "Bu tekrarlayıcı için bir istek zaten devam ediyor", "cliDangerNotConfirmed": "Yıkıcı CLI komutları Tekrarlayıcılar panelinde onay gerektirir.", - "pingFailed": "Ping başarısız olduComment", + "pingFailed": "Ping başarısız oldu", "floodScopeBusy": "Tekrarlayıcı yönetimi veya traceroute devam ederken taşma kapsamını geçersiz kılma kullanılamaz. Boştayken tekrar deneyin." }, "mqttHints": { @@ -3076,7 +3076,7 @@ "maxHopsFilter": "Maks hops", "maxHopsAll": "Tüm hops", "maxHopsOption": "{{count}} hops", - "resetView": "Görünümü Sıfırla", + "resetView": "Görünümü sıfırla", "compactLeafCount": "{{count}} kompakt doğrudan eşler" }, "peerListPanel": { diff --git a/src/renderer/locales/uk/translation.json b/src/renderer/locales/uk/translation.json index 6eddad0fb..6af8b829d 100644 --- a/src/renderer/locales/uk/translation.json +++ b/src/renderer/locales/uk/translation.json @@ -2002,7 +2002,7 @@ }, "repeaterRpcInProgress": "Запит на цей ретранслятор уже виконується", "cliDangerNotConfirmed": "Деструктивні команди CLI вимагають підтвердження на панелі повторювачів.", - "pingFailed": "Невдалий тест на відгукComment", + "pingFailed": "Ping не вдався", "floodScopeBusy": "Перевизначення обсягу затоплення недоступне, поки виконується адміністрування повторювача або трасування. Повторіть спробу під час очікування." }, "mqttHints": { @@ -3078,7 +3078,7 @@ "maxHopsFilter": "Макс. hops", "maxHopsAll": "Усі hops", "maxHopsOption": "{{count}} hops", - "resetView": "Оновити перегляд", + "resetView": "Скинути перегляд", "compactLeafCount": "{{count}} компактні прямі однорангові вузли" }, "peerListPanel": {