diff --git a/.architecture-rules/frontend.yaml b/.architecture-rules/frontend.yaml index 4a4ff87f..8c8274e1 100644 --- a/.architecture-rules/frontend.yaml +++ b/.architecture-rules/frontend.yaml @@ -391,6 +391,41 @@ javascript_rules: exceptions: - "init-alpine.js" + - id: "JS-016" + name: "Do not hardcode 'en-US' (or any locale) in Intl/toLocale calls" + severity: "error" + description: | + Locale-aware APIs (toLocaleDateString, toLocaleString, toLocaleTimeString, + new Intl.NumberFormat, new Intl.DateTimeFormat, etc.) must NOT receive a + hardcoded locale tag like 'en-US'. The user's dashboard language won't be + respected and dates/numbers will render in English even when FR/DE/LB is + selected. + + Use the `I18n.locale` getter from static/shared/js/i18n.js, which returns + the current dashboard language (falls back to 'en' if I18n hasn't loaded). + + WRONG (always renders dates in English): + date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); + new Intl.NumberFormat('en-US').format(num); + + RIGHT: + date.toLocaleDateString(I18n.locale, { year: 'numeric', month: 'short', day: 'numeric' }); + new Intl.NumberFormat(I18n.locale).format(num); + + Suppress with `// noqa: JS-016` on the line for the rare case where a + specific locale is genuinely required (e.g., a US-only invoice number + formatter that must use en-US regardless of UI language). + pattern: + file_pattern: "static/**/js/**/*.js" + anti_patterns: + - "toLocaleDateString\\(\\s*['\"]en-US['\"]" + - "toLocaleString\\(\\s*['\"]en-US['\"]" + - "toLocaleTimeString\\(\\s*['\"]en-US['\"]" + - "new\\s+Intl\\.\\w+\\(\\s*['\"]en-US['\"]" + exceptions: + - "i18n.js" + - "vendor/" + - id: "JS-012" name: "Do not include /api/v1 prefix in API endpoints" severity: "error" diff --git a/scripts/validate/validate_architecture.py b/scripts/validate/validate_architecture.py index 44ecbe3a..50b13aef 100755 --- a/scripts/validate/validate_architecture.py +++ b/scripts/validate/validate_architecture.py @@ -485,6 +485,45 @@ class ArchitectureValidator: # JS-015: Check for native confirm() instead of confirm_modal macros self._check_confirm_usage(file_path, content, lines) + # JS-016: Check for hardcoded 'en-US' in locale-aware APIs + self._check_hardcoded_locale(file_path, content, lines) + + _HARDCODED_LOCALE_RE = re.compile( + r"(toLocaleDateString|toLocaleString|toLocaleTimeString|new\s+Intl\.\w+)\(\s*['\"]en-US['\"]" + ) + + def _check_hardcoded_locale( + self, file_path: Path, content: str, lines: list[str] + ): + """JS-016: Reject hardcoded 'en-US' in toLocale* / Intl.* calls. + + Dates and numbers must respect the user's dashboard language. Use + `I18n.locale` from static/shared/js/i18n.js instead of a hardcoded tag. + + Exceptions: i18n.js itself (defines the helper) and vendor/. + Suppressible per-line with `// noqa: JS-016`. + """ + name = file_path.name + path_str = str(file_path).replace("\\", "/") + if name == "i18n.js" or "/vendor/" in path_str: + return + + for i, line in enumerate(lines, 1): + if not self._HARDCODED_LOCALE_RE.search(line): + continue + if "noqa: js-016" in line.lower() or "noqa: js016" in line.lower(): + continue + self._add_violation( + rule_id="JS-016", + rule_name="Do not hardcode 'en-US' (or any locale) in Intl/toLocale calls", + severity=Severity.ERROR, + file_path=file_path, + line_number=i, + message="Hardcoded 'en-US' ignores the user's dashboard language", + context=line.strip()[:80], + suggestion="Replace 'en-US' with I18n.locale (from static/shared/js/i18n.js)", + ) + def _check_toast_usage(self, file_path: Path, content: str, lines: list[str]): """JS-009: Check for alert() or window.showToast instead of Utils.showToast()""" # Skip utils.js (where showToast is defined) @@ -3287,6 +3326,9 @@ class ArchitectureValidator: # JS-014: Check that store API calls don't include storeCode in path self._check_store_api_paths(file_path, content, lines) + # JS-016: Check for hardcoded 'en-US' in locale-aware APIs + self._check_hardcoded_locale(file_path, content, lines) + def _check_platform_settings_usage( self, file_path: Path, content: str, lines: list[str] ):