From 17b3a8fcf6eda5e28cbf9d01b79fcc8c1e1a8176 Mon Sep 17 00:00:00 2001 From: Dmitrii Bocharov Date: Wed, 1 Jul 2026 15:00:08 +0200 Subject: [PATCH 1/2] feat: warn about invalid ICU placeholders in the editor Show an "Invalid ICU: {value}" message next to the missing-placeholders hint when the edited translation contains text that looks like a placeholder but isn't valid ICU (e.g. `{placeholder:space}`), using the new getInvalidPlaceholders helper from @tginternal/editor. --- .../TranslationsList/TranslationWrite.tsx | 8 +++++++ .../TranslationsTable/TranslationWrite.tsx | 12 +++++++++- .../translations/cell/MissingPlaceholders.tsx | 22 +++++++++++++++++ .../cell/useInvalidPlaceholders.ts | 24 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 webapp/src/views/projects/translations/cell/useInvalidPlaceholders.ts diff --git a/webapp/src/views/projects/translations/TranslationsList/TranslationWrite.tsx b/webapp/src/views/projects/translations/TranslationsList/TranslationWrite.tsx index 8adeb813490..b133156d023 100644 --- a/webapp/src/views/projects/translations/TranslationsList/TranslationWrite.tsx +++ b/webapp/src/views/projects/translations/TranslationsList/TranslationWrite.tsx @@ -14,6 +14,7 @@ import { TranslationEditor } from '../TranslationEditor'; import { MissingPlaceholders } from '../cell/MissingPlaceholders'; import { useMissingPlaceholders } from '../cell/useMissingPlaceholders'; +import { useInvalidPlaceholders } from '../cell/useInvalidPlaceholders'; import { TranslationVisual } from '../translationVisual/TranslationVisual'; import { ControlsEditorReadOnly } from '../cell/ControlsEditorReadOnly'; import { useBaseTranslation } from '../useBaseTranslation'; @@ -131,6 +132,12 @@ export const TranslationWrite: React.FC> = ({ enabled: baseLanguage !== language.tag, }); + const invalidPlaceholders = useInvalidPlaceholders({ + currentTranslation: value, + nested, + enabled: editEnabled, + }); + const handleModeToggle = () => { setMode((mode) => (mode === 'syntax' ? 'placeholders' : 'syntax')); }; @@ -221,6 +228,7 @@ export const TranslationWrite: React.FC> = ({ > = ({ enabled: baseLanguage !== language.tag, }); + const invalidPlaceholders = useInvalidPlaceholders({ + currentTranslation: value, + nested, + enabled: editEnabled, + }); + const translationTasks = keyData.tasks?.filter( (t) => t.languageTag === language.tag ); @@ -158,9 +165,12 @@ export const TranslationWrite: React.FC> = ({ /> )} - {Boolean(missingPlaceholders.length) && ( + {Boolean( + missingPlaceholders.length || invalidPlaceholders.length + ) && ( theme.palette.error.main}; +`; + type Props = { placeholders: Placeholder[]; + invalidPlaceholders: InvalidPlaceholder[]; onPlaceholderClick: (placeholder: Placeholder) => void; locale: string; variant: string | undefined; @@ -27,6 +37,7 @@ type Props = { export const MissingPlaceholders = ({ placeholders, + invalidPlaceholders, onPlaceholderClick, locale, variant, @@ -50,6 +61,17 @@ export const MissingPlaceholders = ({ className={className} onMouseDown={(e) => e.preventDefault()} > + {Boolean(invalidPlaceholders.length) && ( + + p.value).join(', '), + }} + /> + + )} {Boolean(placeholders.length) && ( <> diff --git a/webapp/src/views/projects/translations/cell/useInvalidPlaceholders.ts b/webapp/src/views/projects/translations/cell/useInvalidPlaceholders.ts new file mode 100644 index 00000000000..271a05f1f3e --- /dev/null +++ b/webapp/src/views/projects/translations/cell/useInvalidPlaceholders.ts @@ -0,0 +1,24 @@ +import { InvalidPlaceholder, getInvalidPlaceholders } from '@tginternal/editor'; +import { useMemo } from 'react'; +import { useProject } from 'tg.hooks/useProject'; + +export type Props = { + currentTranslation: string | undefined; + nested: boolean; + enabled: boolean; +}; + +export const useInvalidPlaceholders = ({ + currentTranslation, + nested, + enabled, +}: Props): InvalidPlaceholder[] => { + const icuPlaceholders = useProject().icuPlaceholders; + + return useMemo(() => { + if (!enabled || !icuPlaceholders) { + return []; + } + return getInvalidPlaceholders(currentTranslation || '', nested) ?? []; + }, [currentTranslation, nested, enabled, icuPlaceholders]); +}; From 01032b87e30faaaa77ad5b5955ed65ec3f966366 Mon Sep 17 00:00:00 2001 From: Dmitrii Bocharov Date: Sat, 18 Jul 2026 23:38:58 +0200 Subject: [PATCH 2/2] chore: bump @tginternal/editor to 1.19.0 --- webapp/package-lock.json | 44 +++++++++++++++---- webapp/package.json | 2 +- .../cell/useInvalidPlaceholders.ts | 2 +- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index fd6c0fd7e81..b2a1c9aa0e3 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -29,7 +29,7 @@ "@sentry/react": "^10.31.0", "@sentry/vite-plugin": "^4.6.1", "@stomp/stompjs": "^6.1.2", - "@tginternal/editor": "1.18.0", + "@tginternal/editor": "1.19.0", "@tginternal/library": "file:../library", "@tolgee/format-icu": "^7.1.1", "@tolgee/react": "^7.1.1", @@ -251,6 +251,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -638,6 +639,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz", "integrity": "sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -657,12 +659,14 @@ "node_modules/@codemirror/state": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", - "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", + "peer": true }, "node_modules/@codemirror/view": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.1.tgz", "integrity": "sha512-sBfP4rniPBRQzNakwuQEqjEuiJDWJyF2kqLLqij4WXRoVwPPJfjx966Eq3F7+OPQxDtMt/Q9MWLoZLWjeveBlg==", + "peer": true, "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", @@ -757,6 +761,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -805,6 +810,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -814,6 +820,7 @@ "resolved": "https://registry.npmjs.org/@dicebear/avatars/-/avatars-4.10.2.tgz", "integrity": "sha512-7Qd4Mmq9jeMkWtPZgY89GxFZmu1ERSTSTSEbeNfgVzqxZ0M2NHKcc/uynFMpU13w2BGesCDGT3ulmKyCoYbMGA==", "deprecated": "This package is deprecated. Use '@dicebear/core' instead. Read more: https://dicebear.com/how-to-use/js-library", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.7", "pure-color": "^1.3.0", @@ -858,6 +865,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -960,6 +968,7 @@ "version": "11.11.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -1000,6 +1009,7 @@ "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", @@ -2193,6 +2203,7 @@ "version": "5.16.0", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.0.tgz", "integrity": "sha512-DbR1NckTLpjt9Zut9EGQ70th86HfN0BYQgyYro6aXQrNfjzSwe3BJS1AyBQ5mJ7TdL6YVRqohfukxj9JlqZZUg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/base": "5.0.0-beta.40", @@ -3486,6 +3497,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3543,10 +3555,11 @@ } }, "node_modules/@tginternal/editor": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@tginternal/editor/-/editor-1.18.0.tgz", - "integrity": "sha512-QsA2M1ru9JVkVbiGZPnqVqQ+qFEeCAA9kI3N7XwNXwyPYNfwnPpm5I/Wy4xFagJ9BVEXQcdkE03qFuEomDW+Mg==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@tginternal/editor/-/editor-1.19.0.tgz", + "integrity": "sha512-HoObpct1zDxptiExe5rK4ktdN48FASSkQK5fmw9lg8kr31YY7q2/HutSJjnngkwLHc2aSBxVqixeN+uJxJdMDw==", "dependencies": { + "bidi-js": "^1.0.3", "entities": "^6.0.1" }, "peerDependencies": { @@ -3922,6 +3935,7 @@ "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3942,6 +3956,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4073,6 +4088,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.19.1", "@typescript-eslint/types": "6.19.1", @@ -4460,6 +4476,7 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4812,7 +4829,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, "license": "MIT", "dependencies": { "require-from-string": "^2.0.2" @@ -5061,6 +5077,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -5547,7 +5564,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -5726,6 +5744,7 @@ "version": "2.29.2", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.2.tgz", "integrity": "sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==", + "peer": true, "engines": { "node": ">=0.11" }, @@ -6331,6 +6350,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -10828,6 +10848,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -11025,6 +11046,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11048,6 +11070,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12428,7 +12451,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12614,6 +12636,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -12756,6 +12779,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -13525,6 +13549,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13873,6 +13898,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14349,6 +14375,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14492,6 +14519,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/webapp/package.json b/webapp/package.json index ef1c4cc578f..236d5fd0610 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -28,7 +28,7 @@ "@sentry/react": "^10.31.0", "@sentry/vite-plugin": "^4.6.1", "@stomp/stompjs": "^6.1.2", - "@tginternal/editor": "1.18.0", + "@tginternal/editor": "1.19.0", "@tginternal/library": "file:../library", "@tolgee/format-icu": "^7.1.1", "@tolgee/react": "^7.1.1", diff --git a/webapp/src/views/projects/translations/cell/useInvalidPlaceholders.ts b/webapp/src/views/projects/translations/cell/useInvalidPlaceholders.ts index 271a05f1f3e..e959d21b2cf 100644 --- a/webapp/src/views/projects/translations/cell/useInvalidPlaceholders.ts +++ b/webapp/src/views/projects/translations/cell/useInvalidPlaceholders.ts @@ -19,6 +19,6 @@ export const useInvalidPlaceholders = ({ if (!enabled || !icuPlaceholders) { return []; } - return getInvalidPlaceholders(currentTranslation || '', nested) ?? []; + return getInvalidPlaceholders(currentTranslation || '', nested); }, [currentTranslation, nested, enabled, icuPlaceholders]); };