From 5e032898f86e7fdc4cacf896b0f5e9ca46b204d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 05:24:32 +0000 Subject: [PATCH] integrations keyed, and the Google guide's foreign labels marked (163 -> 96) Applies the rule added to docs/localization.md in 91b9bec9: a label quoted from another product's interface is an identifier, so the prose around it is translated and the label itself is left in English and NOT keyed. Gives that decision a name in the code. states it at each of the ~30 call sites, where whoever next edits the sentence will see it, and gives the scanner something exact to skip -- without which labels that MUST stay English count as debt forever and the area can never be held at zero. That is the same trap as counting new Error() messages. `reason` is a closed union so a second excuse has to be argued for rather than smuggled in as free text. The sentences needed rephrasing, not just splitting. "Go to the [New Project] page." decomposes into "Go to the" + link + "page." -- and the Portuguese value of that second key is "." A key that holds a full stop is the fragmentation smell, so the English now puts the label at a clause boundary ("Open the project creation page:" then the link), which reads naturally in both languages and needs no node-level interpolation. Worth knowing for phase 3: sentences with an inline link are the one shape this catalog format cannot express, and rephrasing is the cheap way out. Scanner: the existing SCREAMING_CONSTANT rule now allows . and - so Google's client-secret prefix example GOCSPX-... falls under it. Same rule, wider class. --- scripts/i18n/scan-hardcoded.ts | 17 +- .../IntegrationsSettings/ApifyConfig.tsx | 8 +- .../IntegrationsSettings/GoogleAccount.tsx | 24 ++- .../GoogleOAuthConfig.tsx | 200 ++++++++---------- src/apps/officer-web/lib/Untranslated.tsx | 28 +++ src/apps/officer-web/locales/en-GB.json | 74 +++++++ src/apps/officer-web/locales/pt-PT.json | 74 +++++++ 7 files changed, 298 insertions(+), 127 deletions(-) create mode 100644 src/apps/officer-web/lib/Untranslated.tsx diff --git a/scripts/i18n/scan-hardcoded.ts b/scripts/i18n/scan-hardcoded.ts index 7dc5711a..f7617247 100755 --- a/scripts/i18n/scan-hardcoded.ts +++ b/scripts/i18n/scan-hardcoded.ts @@ -80,7 +80,8 @@ function isHuman(value: string | undefined): value is string { if (!/[A-Za-z]{3}/.test(s)) return false; if (/^[a-z0-9\-_./:]+$/.test(s)) return false; if (/^(https?:|\/|#|data:|application\/|image\/)/.test(s)) return false; - if (/^[A-Z0-9_]+$/.test(s)) return false; + // Allows . and - so credential-format examples like `GOCSPX-...` fall under the same rule. + if (/^[A-Z0-9_.-]+$/.test(s)) return false; // `Bearer ${token}` is an Authorization header value, not a sentence. Caught by the template-string // pattern because it starts with a capital and interpolates; excluded here because an auth scheme // followed by a credential is never something a person reads. @@ -121,6 +122,18 @@ const stripComments = (source: string): string => const stripCodeElements = (source: string): string => source.replace(/<(code|kbd|pre|samp)\b[^>]*>[\s\S]*?<\/\1>/g, '<$1/>'); +/** + * `` marks text that must stay in one language — today, a label quoted from + * another product's UI, which docs/localization.md classifies as an identifier. + * + * Skipped for the same reason `neverTranslate` categories are excluded from `--check`: counting work the + * doc FORBIDS doing against a target of zero makes the target unreachable. The marker is explicit and + * carries a reason, so this cannot become a quiet catch-all — unlike a heuristic, somebody has to write + * it and say why. + */ +const stripUntranslated = (source: string): string => + source.replace(/]*>[\s\S]*?<\/Untranslated>/g, ''); + function* walk(dir: string): Generator { let entries: string[]; try { @@ -152,7 +165,7 @@ function scan(root: string): { files: number; hits: Hit[] } { files++; let source: string; try { - source = stripCodeElements(stripComments(readFileSync(file, 'utf8'))); + source = stripUntranslated(stripCodeElements(stripComments(readFileSync(file, 'utf8')))); } catch { continue; } diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx index d9d8c9e1..5b2e6fcd 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx @@ -5,6 +5,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { useClient } from 'hooks/useClient'; +import { useTranslation } from '@/lib/i18n'; type ApifyConfigData = { apiToken: string; @@ -15,6 +16,7 @@ type ApifyStatus = { }; export const ApifyConfig = () => { + const { t } = useTranslation(); const client = useClient(); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -38,10 +40,10 @@ export const ApifyConfig = () => { setIsSaving(true); try { await client.put('/integrations/apify/config', { apiToken: apiToken.trim() }); - toast.success('Apify API token saved'); + toast.success(t('settings.apify.saved')); setStatus({ configured: !!apiToken.trim() }); } catch { - toast.error('Failed to save Apify API token'); + toast.error(t('settings.apify.saveFailed')); } finally { setIsSaving(false); } @@ -64,7 +66,7 @@ export const ApifyConfig = () => {