Repository navigation
[Maintainer] i18n: fix scoped-locale and decimal Bugbot findings from #1148 #1162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,12 @@ | ||
| import { expect, test } from 'bun:test' | ||
| import { parseReferenceScaleLength } from './floorplan-panel' | ||
| import { formatFloorplanNumber, parseReferenceScaleLength } from './floorplan-panel' | ||
|
|
||
| test('floor-plan reference scales parse with the scoped locale', () => { | ||
| expect(parseReferenceScaleLength('1,000', 'meters', 'fr')).toBe(1) | ||
| expect(parseReferenceScaleLength('1,000', 'meters', 'en')).toBe(1000) | ||
| }) | ||
|
|
||
| test('floor-plan reference scales display with the scoped locale', () => { | ||
| expect(formatFloorplanNumber(2.75, 'fr')).toBe('2,75') | ||
| expect(formatFloorplanNumber(2.75, 'en')).toBe('2.75') | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| 'use client' | ||
|
|
||
| import { parseLocalizedNumber, useLocale, useScene } from '@pascal-app/core' | ||
| import { formatNumber, parseLocalizedNumber, useLocale, useScene } from '@pascal-app/core' | ||
| import { useCallback, useEffect, useRef, useState } from 'react' | ||
| import { lingoUnitSpec, measurementHint, parseMeasurement } from '../../../lib/measurement-parser' | ||
| import { useLinearDisplay } from '../../../lib/use-linear-display' | ||
|
|
@@ -35,6 +35,19 @@ export function parseMetricInput( | |
| : parseLocalizedNumber(text, options.locale) | ||
| } | ||
|
|
||
| export function formatMetricInputValue( | ||
| value: number, | ||
| precision: number, | ||
| locale: string, | ||
| ): string { | ||
| return formatNumber(value, { locale, min: precision, max: precision }) | ||
| } | ||
|
|
||
| function roundToPrecision(value: number, precision: number): number { | ||
| const factor = 10 ** precision | ||
| return Math.round((value + Number.EPSILON) * factor) / factor | ||
|
Comment on lines
+46
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For metric controls that permit negative values, such as node-position controls, Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| export function MetricControl({ | ||
| label, | ||
| value, | ||
|
|
@@ -67,16 +80,20 @@ export function MetricControl({ | |
| ) | ||
| const roundStoredValueForDisplayPrecision = useCallback( | ||
| (storedValue: number) => | ||
| clamp(toStoredValue(Number.parseFloat(toDisplayValue(storedValue).toFixed(precision)))), | ||
| clamp(toStoredValue(roundToPrecision(toDisplayValue(storedValue), precision))), | ||
| [clamp, precision, toDisplayValue, toStoredValue], | ||
| ) | ||
|
|
||
| const displayValue = toDisplayValue(value) | ||
| const formatDisplayValue = useCallback( | ||
| (nextValue: number) => formatMetricInputValue(nextValue, precision, locale), | ||
| [locale, precision], | ||
| ) | ||
|
|
||
| const [isEditing, setIsEditing] = useState(false) | ||
| const [isDragging, setIsDragging] = useState(false) | ||
| const [isHovered, setIsHovered] = useState(false) | ||
| const [inputValue, setInputValue] = useState(displayValue.toFixed(precision)) | ||
| const [inputValue, setInputValue] = useState(() => formatDisplayValue(displayValue)) | ||
| const startXRef = useRef(0) | ||
| const startValueRef = useRef(0) | ||
| const containerRef = useRef<HTMLDivElement>(null) | ||
|
|
@@ -97,9 +114,9 @@ export function MetricControl({ | |
|
|
||
| useEffect(() => { | ||
| if (!isEditing) { | ||
| setInputValue(displayValue.toFixed(precision)) | ||
| setInputValue(formatDisplayValue(displayValue)) | ||
| } | ||
| }, [displayValue, precision, isEditing]) | ||
| }, [displayValue, formatDisplayValue, isEditing]) | ||
|
|
||
| useEffect(() => { | ||
| const container = containerRef.current | ||
|
|
@@ -237,8 +254,8 @@ export function MetricControl({ | |
|
|
||
| const handleValueClick = useCallback(() => { | ||
| setIsEditing(true) | ||
| setInputValue(toDisplayValue(value).toFixed(precision)) | ||
| }, [value, toDisplayValue, precision]) | ||
| setInputValue(formatDisplayValue(toDisplayValue(value))) | ||
| }, [formatDisplayValue, value, toDisplayValue]) | ||
|
|
||
| const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { | ||
| setInputValue(e.target.value) | ||
|
|
@@ -249,7 +266,7 @@ export function MetricControl({ | |
| const parsed = parseMetricInput(inputValue, unit, { isImperial, locale, parseUnit }) | ||
| const stored = parsed === null ? null : spec ? parsed : toStoredValue(parsed) | ||
| if (stored === null) { | ||
| setInputValue(toDisplayValue(value).toFixed(precision)) | ||
| setInputValue(formatDisplayValue(toDisplayValue(value))) | ||
| } else { | ||
| applyCommittedValue(clamp(stored)) | ||
| } | ||
|
|
@@ -264,7 +281,7 @@ export function MetricControl({ | |
| clamp, | ||
| toStoredValue, | ||
| value, | ||
| precision, | ||
| formatDisplayValue, | ||
| toDisplayValue, | ||
| ]) | ||
|
|
||
|
|
@@ -290,25 +307,25 @@ export function MetricControl({ | |
| if (e.key === 'Enter') { | ||
| submitValue() | ||
| } else if (e.key === 'Escape') { | ||
| setInputValue(toDisplayValue(value).toFixed(precision)) | ||
| setInputValue(formatDisplayValue(toDisplayValue(value))) | ||
| setIsEditing(false) | ||
| } else if (e.key === 'ArrowUp') { | ||
| e.preventDefault() | ||
| const newV = clamp(value + toStoredValue(step)) | ||
| applyCommittedValue(newV) | ||
| setInputValue(toDisplayValue(newV).toFixed(precision)) | ||
| setInputValue(formatDisplayValue(toDisplayValue(newV))) | ||
| } else if (e.key === 'ArrowDown') { | ||
| e.preventDefault() | ||
| const newV = clamp(value - toStoredValue(step)) | ||
| applyCommittedValue(newV) | ||
| setInputValue(toDisplayValue(newV).toFixed(precision)) | ||
| setInputValue(formatDisplayValue(toDisplayValue(newV))) | ||
| } | ||
| }, | ||
| [ | ||
| submitValue, | ||
| value, | ||
| toDisplayValue, | ||
| precision, | ||
| formatDisplayValue, | ||
| step, | ||
| clamp, | ||
| applyCommittedValue, | ||
|
|
@@ -373,7 +390,7 @@ export function MetricControl({ | |
| onClick={handleValueClick} | ||
| > | ||
| <span className="font-mono tabular-nums tracking-tight"> | ||
| {Number(displayValue.toFixed(precision)).toFixed(precision)} | ||
| {formatDisplayValue(displayValue)} | ||
| </span> | ||
| {displayUnit && <span className="ml-[1px] text-muted-foreground">{displayUnit}</span>} | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a reference scale is created under French, this stores a value such as
2,75 minscaleReference.label; the floor-plan layer and reference panel subsequently render that persisted string verbatim. If the editor locale later changes, or another user opens the scene under a different locale, existing labels retain the creator's decimal convention and scene serialization now varies by locale. Persist the numeric value and unit (or a locale-neutral representation) and format the label with the active locale at render time.Useful? React with 👍 / 👎.