diff --git a/.changeset/6888-location-residue-refusal-keyed.md b/.changeset/6888-location-residue-refusal-keyed.md new file mode 100644 index 0000000000..125bddc61c --- /dev/null +++ b/.changeset/6888-location-residue-refusal-keyed.md @@ -0,0 +1,48 @@ +--- +'@object-ui/fields': patch +'@object-ui/i18n': patch +--- + +Key `LocationField`'s THIRD refusal sentence — the residue arm — into the locale +packs (objectui#6888). + +Typing a half that is only PARTLY a number (`12abc, 34`) is refused by +`LocationField` with its own sentence, added by objectui#6715. objectui#6755 had +ruled two weeks earlier that a widget's own refusal sentence goes through +`useFieldTranslation` + `FIELD_DEFAULTS`, and named three sentences — but it was +written on 2026-08-29 14:53 and this arm landed after, so it stayed a hard-coded +English literal while its two siblings were keyed. + +**The consequence was worse than one more English string.** All three refusal arms +render through the SAME `
` and the same `refusalError` state, so after #6755 +landed that one line spoke the reader's language when the format or range arm fired +and English when the residue arm did — objectui#4028's shape ("four Chinese labels +around one English one") compressed into a single sentence position, which reads to +a user as a bug rather than as a missing translation. + +**Arity is answered explicitly, not defaulted.** Unlike the other two sentences, this +one had grammatical number: `verb` was `is not a number` / `are not numbers`, chosen +in TypeScript. Handing a pack an English verb form through a `{{hole}}` gives it a +fragment it cannot inflect around — Arabic has a DUAL, and two halves is exactly that +case. So the verb is not a hole. It lives inside two SIBLING keys picked at the call +site, `fields.location.refusedResidue` and `fields.location.refusedResidueOne`, +following this repo's own plural convention rather than i18next's `_one`/`_other` +suffixes — the same shape `RecordPickerDialog` already uses in this very defaults map +(`lookup.recordCount` / `lookup.recordCountOne`), and for the reason `ReactionPicker` +states in source: zh/ja/ko have no separate singular form, would legitimately omit a +`_one` half, and `all-locales-key-parity` reads that as a missing key. The `ar` pack +now uses its dual (`ليسا رقمين`), which the old implementation could not have produced. + +The English conjunction `' and '` and the coordinate NOUNS go the same way: each pack +writes its own conjunction inside the two-half value, and `latitude` / `longitude` +become `fields.location.latitude` / `fields.location.longitude`, keyed once each and +interpolated into both arities so no locale holds two spellings of the same word. The +only holes carrying untranslated data are `{{text}}` / `{{otherText}}` — the +characters the person actually typed. + +**No behaviour moves.** The English values are byte-identical to the literal they +replace in both arities, verified by `check:i18n-drift` (0 en values changed, 4 added) +and by objectui#6715's own `LocationField.strictNumeric.test.tsx` and `plugin-form`'s +`ObjectForm.locationResidue.test.tsx` passing untouched. Provider-less rendering is +unchanged, the refusal itself is unchanged, and the four new keys are bound from here +on by `check:i18n-keys` and `all-locales-key-parity` like their three siblings. diff --git a/packages/fields/src/__tests__/LocationField.residueI18n-6888.test.tsx b/packages/fields/src/__tests__/LocationField.residueI18n-6888.test.tsx new file mode 100644 index 0000000000..223bccfbb8 --- /dev/null +++ b/packages/fields/src/__tests__/LocationField.residueI18n-6888.test.tsx @@ -0,0 +1,314 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#6888 — `LocationField`'s THIRD refusal sentence reaches the packs. + * + * objectui#6755 ruled that a widget's own refusal sentence goes through + * `useFieldTranslation` + `FIELD_DEFAULTS`, and named three sentences. The + * residue arm (`12abc, 34` — a half that is only PARTLY a number) landed from + * objectui#6715 AFTER that ruling was written, so it was outside the count and + * stayed a literal. Triage ruled applying the same principle to it in-lane + * execution rather than a new adjudication. + * + * The consequence this file pins is not "one more English string": all three + * arms render through the SAME `
` and the same `refusalError`, so one line
+ * spoke the reader's language on two arms and English on the third —
+ * objectui#4028's shape ("four Chinese labels around one English one")
+ * compressed into a single sentence position.
+ *
+ * ## ⭐ The arity question this arm had to answer, and what is asserted about it
+ *
+ * The other two sentences have no grammatical number. This one did: `verb` was
+ * `is not a number` / `are not numbers`, chosen in TypeScript. Passing that
+ * through a `{{hole}}` hands a pack an English verb form it cannot inflect
+ * around — Arabic has a DUAL, and two halves is exactly the case where it
+ * applies.
+ *
+ * The answer taken, and asserted below, is this repo's OWN plural convention:
+ * two SIBLING keys picked at the call site (`refusedResidue` /
+ * `refusedResidueOne`), ⛔ not an i18next `_one`/`_other` family. The precedent
+ * followed is `RecordPickerDialog`'s `lookup.recordCount` / `recordCountOne` —
+ * the same package, the same `FIELD_DEFAULTS` map — with
+ * `list.recordCount`/`recordCountOne` and `detail.reactionCount`/
+ * `reactionCountOne` as the same shape elsewhere. `ReactionPicker` states the
+ * reason in source: zh/ja/ko have no separate singular form, would legitimately
+ * omit a `_one` half, and `all-locales-key-parity` reads that as a missing key.
+ *
+ * ⭐ `ARITY_IS_IN_THE_VALUE` below is the assertion that distinguishes this
+ * answer from the one that was NOT taken. If the verb were a hole, every pack's
+ * two sentences would differ only where the widget substituted an English word,
+ * and `ar`'s dual could not exist at all. So the test demands that each pack's
+ * one-half and two-half sentences differ in their own script — and pins the
+ * Arabic dual (`ليسا رقمين`) by name.
+ *
+ * ## Shape inherited from `widget-diagnostics-i18n-6755.test.tsx`
+ *
+ * - **Positive AND negative together.** `createSafeTranslation` falls back to
+ * `FIELD_DEFAULTS[key]`, which is byte-identical to the literal it replaced —
+ * so a pack that resolves NOTHING renders exactly what the hard-coded string
+ * used to. Only "says the translated thing" plus "no English survives"
+ * separates keyed from still-hard-coded.
+ * - **A positive control for the pack read, in this run.** `AddressField`'s
+ * `fields.address.*` keys were keyed by objectui#4028; if the provider were
+ * dead, that control fails too and no negative assertion here could be read
+ * as a pass.
+ * - **`en` and provider-less are NO-OP pins.** The English values are
+ * byte-identical to the literal, which is why objectui#6715's own
+ * `LocationField.strictNumeric.test.tsx` and `plugin-form`'s
+ * `ObjectForm.locationResidue.test.tsx` are untouched by this card. Asserting
+ * them here states that as a fact of THIS change rather than a hope.
+ */
+import React from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, cleanup } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { I18nProvider, builtInLocales } from '@object-ui/i18n';
+
+import { LocationField } from '../widgets/LocationField';
+import { AddressField } from '../widgets/AddressField';
+
+const locationField = { name: 'site', label: 'Site', type: 'location' } as any;
+const addressField = { name: 'billing_address', type: 'address' } as any;
+
+/** Text whose FIRST half is a number with residue after it; the second is clean. */
+const ONE_HALF = '12abc, 34';
+/** Text whose SECOND half is the offending one — the other order. */
+const OTHER_HALF = '30.27, 120abc';
+/** Text where BOTH halves carry residue. */
+const BOTH_HALVES = '12abc, 34xyz';
+
+/** The English sentences this card keyed — byte-identical to the old literal. */
+const EN_ONE_HALF =
+ 'Not saved: latitude "12abc" is not a number. Enter plain decimals (example: 30.2741, 120.1551).';
+const EN_OTHER_HALF =
+ 'Not saved: longitude "120abc" is not a number. Enter plain decimals (example: 30.2741, 120.1551).';
+const EN_BOTH_HALVES =
+ 'Not saved: latitude "12abc" and longitude "34xyz" are not numbers. ' +
+ 'Enter plain decimals (example: 30.2741, 120.1551).';
+
+/**
+ * English fragments that must NOT survive a translated render.
+ *
+ * The verb and the conjunction are the grammar the old implementation chose in
+ * TypeScript; the two nouns are the words it never let a pack see. All four are
+ * the defect, so all four are asserted absent rather than just the sentence.
+ */
+const ENGLISH_FRAGMENTS = ['is not a number', 'are not numbers', ' and ', 'latitude', 'longitude'];
+
+function renderIn(language: string, element: React.ReactElement) {
+ return render(
+
` would have made the mixed
+ // languages two separate lines rather than one, and far less confusing.
+ const { container } = renderIn('zh',
` and one
+ * `refusalError`, so leaving this one a literal made a single line speak the
+ * reader's language on two arms and English on the third.
+ *
+ * ⭐ ARITY — the one question the other two arms did not have to answer.
+ * `verb` was English GRAMMAR (`is not a number` / `are not numbers`), and a
+ * pack whose plural rules differ cannot inflect around an English verb form
+ * handed to it through a hole. So the verb is not a hole: it lives inside two
+ * SIBLING KEYS picked here, at the call site.
+ *
+ * ⛔ Deliberately NOT i18next's `_one`/`_other` suffixes. This repo's own
+ * plural convention is a `X` / `XOne` pair branched at the call site —
+ * `lookup.recordCount`/`recordCountOne` in this very defaults map
+ * (`RecordPickerDialog`), `list.recordCount`/`recordCountOne`,
+ * `detail.reactionCount`/`reactionCountOne` — because zh/ja/ko have no separate
+ * singular form and would legitimately omit a `_one` half, which
+ * `all-locales-key-parity` reads as a missing key (objectstack#5430). The
+ * suffix form also needs a base key to cover the categories no pack enumerates
+ * (`ru` few/many, `ar` two/zero — objectui#3863); the sibling pair needs none.
+ *
+ * The arity here is exactly binary and always will be: `residue` is
+ * `COORDINATE_LABELS` filtered, and a draft that is not two comma-separated
+ * parts never reaches this arm. So `ar` can use its DUAL in the two-half value,
+ * which is precisely what an English `verb` hole made impossible.
+ *
+ * `named`'s two components are split the same way rather than pre-joined:
+ * `' and '` was an English conjunction and `latitude`/`longitude` are
+ * translatable nouns (every pack already spells them inside its own
+ * `refusedFormat`). The nouns are keyed ONCE each and interpolated into both
+ * values, so no locale has to keep two spellings of the same word in step. The
+ * only holes carrying untranslated data are `{{text}}`/`{{otherText}}` — the
+ * characters the person actually typed, which no pack should touch.
+ */
+function refusedResidueMessage(t: TranslateFn, residue: readonly ResidueHalf[]): string {
+ const [first, second] = residue.map(half => ({ name: coordinateName(t, half.label), text: half.text }));
+ if (!second) return t('fields.location.refusedResidueOne', { name: first.name, text: first.text });
+ return t('fields.location.refusedResidue', {
+ name: first.name,
+ text: first.text,
+ otherName: second.name,
+ otherText: second.text,
+ });
+}
+
+/**
+ * The reader's word for one coordinate.
+ *
+ * ⛔ Written as a branch over STRING LITERALS rather than a computed
+ * `t(`fields.location.${label}`)`: `check:i18n-keys` and `check:i18n-dead-keys`
+ * both read `t()` literals, so a template key would be invisible to the gate
+ * that proves it resolves AND to the one that proves it is still used.
*/
-function refusedResidueMessage(residue: readonly ResidueHalf[]): string {
- const named = residue.map(half => `${half.label} "${half.text}"`).join(' and ');
- const verb = residue.length > 1 ? 'are not numbers' : 'is not a number';
- return `Not saved: ${named} ${verb}. Enter plain decimals (example: 30.2741, 120.1551).`;
+function coordinateName(t: TranslateFn, label: CoordinateLabel): string {
+ return label === 'latitude' ? t('fields.location.latitude') : t('fields.location.longitude');
}
/**
@@ -452,7 +507,7 @@ export function LocationField({ value, onChange, field, readonly, error, ...prop
// `setRefusalError` the other two arms use — a third SILENT refusal is
// precisely the defect objectui#6716 had just finished removing, which is
// why this card was held until #6716 landed.
- setRefusalError(refusedResidueMessage(parsed.residue));
+ setRefusalError(refusedResidueMessage(t as TranslateFn, parsed.residue));
return;
}
diff --git a/packages/fields/src/widgets/useFieldTranslation.ts b/packages/fields/src/widgets/useFieldTranslation.ts
index bb6fe1fdab..9c2b0e3540 100644
--- a/packages/fields/src/widgets/useFieldTranslation.ts
+++ b/packages/fields/src/widgets/useFieldTranslation.ts
@@ -107,6 +107,28 @@ const FIELD_DEFAULTS: Record `, so a literal here made ONE line
+ // bilingual rather than one screen.
+ //
+ // Two SIBLING keys, not an i18next `_one`/`_other` family: the verb is
+ // English grammar and cannot be a hole, and this repo's plural convention is
+ // the `X`/`XOne` pair branched at the call site (`lookup.recordCount` /
+ // `recordCountOne` above is the same map's own instance). The coordinate
+ // NOUNS are keyed once each and interpolated into both, so a locale spells
+ // `latitude` in exactly one place. `{{text}}`/`{{otherText}}` are what the
+ // person typed and stay untouched by every pack.
+ //
+ // Values are byte-identical to the literal they replace, in both arities:
+ // `Not saved: latitude "12abc" is not a number. …` and
+ // `… latitude "12abc" and longitude "34xyz" are not numbers. …`.
+ 'fields.location.latitude': 'latitude',
+ 'fields.location.longitude': 'longitude',
+ 'fields.location.refusedResidueOne':
+ 'Not saved: {{name}} "{{text}}" is not a number. Enter plain decimals (example: 30.2741, 120.1551).',
+ 'fields.location.refusedResidue':
+ 'Not saved: {{name}} "{{text}}" and {{otherName}} "{{otherText}}" are not numbers. Enter plain decimals (example: 30.2741, 120.1551).',
// objectui#3342 — the tags widget's input hint. Used only when the field
// author declared no `placeholder` of their own (author declaration wins).
'fields.tags.placeholder': 'Type and press Enter to add…',
diff --git a/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts b/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
index 5b526b346c..79225aeb24 100644
--- a/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
+++ b/packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts
@@ -279,7 +279,13 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// placeholder suggests („Falsche Datensatz-ID — …“) — one LITERAL span, like
// `timeline.unsupported.objectBoundGantt` above rather than the interpolated
// ones, because the quoted thing is sample prose this pack authored.
- expect(okSpans, 'correctly paired spans').toBe(59);
+ // 62 once objectui#6888 keyed `LocationField`'s residue refusal: the
+ // one-half sentence quotes the offending text („{{text}}“) and the two-half
+ // one quotes both („{{text}}“ / „{{otherText}}“) — three interpolated
+ // spans, all runtime data, like the `ActivityTimeline` pair above. The two
+ // new coordinate NOUN keys (`fields.location.latitude`/`longitude`) carry
+ // no quotes at all: they are interpolated INTO those spans, not around them.
+ expect(okSpans, 'correctly paired spans').toBe(62);
});
it('keeps the count identity that replaces the card’s count(„) === count(“)', () => {
@@ -305,7 +311,9 @@ describe('objectui#3876 — de pack closes „ with “ and not with a straight
// each new value added a MATCHED „…“ pair, not a stray closer that would
// have made `close === open` true for the wrong reason. 59 / 59 / 0 after
// objectui#7173 added `aiApprovals.rejectPlaceholder`, one more matched pair.
- expect({ open, close, rdq }).toEqual({ open: 59, close: 59, rdq: 0 });
+ // 62 / 62 / 0 after objectui#6888 keyed `LocationField`'s residue refusal —
+ // three more matched pairs across its two arity siblings, and `rdq` still 0.
+ expect({ open, close, rdq }).toEqual({ open: 62, close: 62, rdq: 0 });
// The durable shape: every „ closed by a “, every surplus “ an English
// opener answered by a ”. Survived translating the two English values.
expect(close).toBe(open + rdq);
diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts
index 70a769597d..591a972bc9 100644
--- a/packages/i18n/src/locales/ar.ts
+++ b/packages/i18n/src/locales/ar.ts
@@ -281,6 +281,12 @@ const ar = {
refusedFormat:
"لم يتم الحفظ: أدخل زوجًا من خط العرض وخط الطول (مثال: 30.2741, 120.1551).",
refusedRange: "لم يتم الحفظ: {{detail}}",
+ latitude: "خط العرض",
+ longitude: "خط الطول",
+ refusedResidueOne:
+ "لم يتم الحفظ: {{name}} «{{text}}» ليس رقمًا. أدخل أرقامًا عشرية عادية (مثال: 30.2741, 120.1551).",
+ refusedResidue:
+ "لم يتم الحفظ: {{name}} «{{text}}» و{{otherName}} «{{otherText}}» ليسا رقمين. أدخل أرقامًا عشرية عادية (مثال: 30.2741, 120.1551).",
},
tags: {
placeholder: "اكتب واضغط Enter للإضافة…",
diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts
index cc9e0fae3f..6ca1653c68 100644
--- a/packages/i18n/src/locales/de.ts
+++ b/packages/i18n/src/locales/de.ts
@@ -277,6 +277,12 @@ const de = {
refusedFormat:
"Nicht gespeichert: Geben Sie ein Paar aus Breitengrad, Längengrad ein (Beispiel: 30.2741, 120.1551).",
refusedRange: "Nicht gespeichert: {{detail}}",
+ latitude: "Breitengrad",
+ longitude: "Längengrad",
+ refusedResidueOne:
+ "Nicht gespeichert: {{name}} „{{text}}“ ist keine Zahl. Geben Sie einfache Dezimalzahlen ein (Beispiel: 30.2741, 120.1551).",
+ refusedResidue:
+ "Nicht gespeichert: {{name}} „{{text}}“ und {{otherName}} „{{otherText}}“ sind keine Zahlen. Geben Sie einfache Dezimalzahlen ein (Beispiel: 30.2741, 120.1551).",
},
tags: {
placeholder: "Tippen und mit der Eingabetaste hinzufügen…",
diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts
index 165f8ce996..fc01d88b54 100644
--- a/packages/i18n/src/locales/en.ts
+++ b/packages/i18n/src/locales/en.ts
@@ -338,6 +338,12 @@ const en = {
refusedFormat:
'Not saved: enter a latitude, longitude pair (example: 30.2741, 120.1551).',
refusedRange: 'Not saved: {{detail}}',
+ latitude: 'latitude',
+ longitude: 'longitude',
+ refusedResidueOne:
+ 'Not saved: {{name}} "{{text}}" is not a number. Enter plain decimals (example: 30.2741, 120.1551).',
+ refusedResidue:
+ 'Not saved: {{name}} "{{text}}" and {{otherName}} "{{otherText}}" are not numbers. Enter plain decimals (example: 30.2741, 120.1551).',
},
// objectui#3342 — the tags widget's input hint, shown while the tag list
// is empty. The author-declared `field.placeholder` always wins over this.
diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts
index 9f53385868..ea322ee56f 100644
--- a/packages/i18n/src/locales/es.ts
+++ b/packages/i18n/src/locales/es.ts
@@ -281,6 +281,12 @@ const es = {
refusedFormat:
"No guardado: introduce un par latitud, longitud (ejemplo: 30.2741, 120.1551).",
refusedRange: "No guardado: {{detail}}",
+ latitude: "latitud",
+ longitude: "longitud",
+ refusedResidueOne:
+ "No guardado: {{name}} «{{text}}» no es un número. Introduce decimales simples (ejemplo: 30.2741, 120.1551).",
+ refusedResidue:
+ "No guardado: {{name}} «{{text}}» y {{otherName}} «{{otherText}}» no son números. Introduce decimales simples (ejemplo: 30.2741, 120.1551).",
},
tags: {
placeholder: "Escriba y pulse Intro para añadir…",
diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts
index f9ebeab6a0..10d1387348 100644
--- a/packages/i18n/src/locales/fr.ts
+++ b/packages/i18n/src/locales/fr.ts
@@ -277,6 +277,12 @@ const fr = {
refusedFormat:
"Non enregistré : saisissez une paire latitude, longitude (exemple : 30.2741, 120.1551).",
refusedRange: "Non enregistré : {{detail}}",
+ latitude: "latitude",
+ longitude: "longitude",
+ refusedResidueOne:
+ "Non enregistré : {{name}} « {{text}} » n'est pas un nombre. Saisissez des décimales simples (exemple : 30.2741, 120.1551).",
+ refusedResidue:
+ "Non enregistré : {{name}} « {{text}} » et {{otherName}} « {{otherText}} » ne sont pas des nombres. Saisissez des décimales simples (exemple : 30.2741, 120.1551).",
},
tags: {
placeholder: "Saisissez puis appuyez sur Entrée pour ajouter…",
diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts
index 1ec0420a9b..e38918e0e5 100644
--- a/packages/i18n/src/locales/ja.ts
+++ b/packages/i18n/src/locales/ja.ts
@@ -277,6 +277,12 @@ const ja = {
refusedFormat:
"保存されていません: 緯度, 経度 の組で入力してください(例: 30.2741, 120.1551)。",
refusedRange: "保存されていません: {{detail}}",
+ latitude: "緯度",
+ longitude: "経度",
+ refusedResidueOne:
+ "保存されていません: {{name}}「{{text}}」は数値ではありません。通常の小数で入力してください(例: 30.2741, 120.1551)。",
+ refusedResidue:
+ "保存されていません: {{name}}「{{text}}」と{{otherName}}「{{otherText}}」は数値ではありません。通常の小数で入力してください(例: 30.2741, 120.1551)。",
},
tags: {
placeholder: "入力してEnterキーで追加…",
diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts
index 9afcaeea42..9c0054f2af 100644
--- a/packages/i18n/src/locales/ko.ts
+++ b/packages/i18n/src/locales/ko.ts
@@ -277,6 +277,12 @@ const ko = {
refusedFormat:
"저장되지 않았습니다: 위도, 경도 쌍으로 입력하세요(예: 30.2741, 120.1551).",
refusedRange: "저장되지 않았습니다: {{detail}}",
+ latitude: "위도",
+ longitude: "경도",
+ refusedResidueOne:
+ "저장되지 않았습니다: {{name}} “{{text}}”은(는) 숫자가 아닙니다. 일반 소수로 입력하세요(예: 30.2741, 120.1551).",
+ refusedResidue:
+ "저장되지 않았습니다: {{name}} “{{text}}”과(와) {{otherName}} “{{otherText}}”은(는) 숫자가 아닙니다. 일반 소수로 입력하세요(예: 30.2741, 120.1551).",
},
tags: {
placeholder: "입력 후 Enter 키로 추가…",
diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts
index 84d2c600cf..0a138ba78c 100644
--- a/packages/i18n/src/locales/pt.ts
+++ b/packages/i18n/src/locales/pt.ts
@@ -276,6 +276,12 @@ const pt = {
refusedFormat:
"Não salvo: informe um par latitude, longitude (exemplo: 30.2741, 120.1551).",
refusedRange: "Não salvo: {{detail}}",
+ latitude: "latitude",
+ longitude: "longitude",
+ refusedResidueOne:
+ "Não salvo: {{name}} “{{text}}” não é um número. Informe decimais simples (exemplo: 30.2741, 120.1551).",
+ refusedResidue:
+ "Não salvo: {{name}} “{{text}}” e {{otherName}} “{{otherText}}” não são números. Informe decimais simples (exemplo: 30.2741, 120.1551).",
},
tags: {
placeholder: "Digite e pressione Enter para adicionar…",
diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts
index dacdb403b6..33ea12739f 100644
--- a/packages/i18n/src/locales/ru.ts
+++ b/packages/i18n/src/locales/ru.ts
@@ -283,6 +283,12 @@ const ru = {
refusedFormat:
"Не сохранено: введите пару широта, долгота (например: 30.2741, 120.1551).",
refusedRange: "Не сохранено: {{detail}}",
+ latitude: "широта",
+ longitude: "долгота",
+ refusedResidueOne:
+ "Не сохранено: {{name}} «{{text}}» — не число. Введите обычные десятичные дроби (например: 30.2741, 120.1551).",
+ refusedResidue:
+ "Не сохранено: {{name}} «{{text}}» и {{otherName}} «{{otherText}}» — не числа. Введите обычные десятичные дроби (например: 30.2741, 120.1551).",
},
tags: {
placeholder: "Введите и нажмите Enter, чтобы добавить…",
diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts
index 9fbfd21dba..c97497c7db 100644
--- a/packages/i18n/src/locales/zh.ts
+++ b/packages/i18n/src/locales/zh.ts
@@ -284,6 +284,12 @@ const zh = {
refusedFormat:
'未保存:请输入纬度, 经度坐标对(例如 30.2741, 120.1551)。',
refusedRange: '未保存:{{detail}}',
+ latitude: '纬度',
+ longitude: '经度',
+ refusedResidueOne:
+ '未保存:{{name}}“{{text}}”不是数字。请输入普通小数(例如 30.2741, 120.1551)。',
+ refusedResidue:
+ '未保存:{{name}}“{{text}}”和{{otherName}}“{{otherText}}”不是数字。请输入普通小数(例如 30.2741, 120.1551)。',
},
tags: {
placeholder: '输入后回车添加…',