diff --git a/.changeset/9664-search-items-available-plural.md b/.changeset/9664-search-items-available-plural.md
new file mode 100644
index 0000000000..5c893e515f
--- /dev/null
+++ b/.changeset/9664-search-items-available-plural.md
@@ -0,0 +1,37 @@
+---
+'@object-ui/i18n': patch
+'@object-ui/app-shell': patch
+---
+
+Full-page search now agrees with its own number while browsing: at exactly one
+searchable item the header reads `1 item available`, not `1 items available`
+(objectui#9664).
+
+The header is one ternary with two branches. The query branch already chose its
+key on `totalCount === 1` — `search.resultsCount` against
+`search.resultsCountPlural`. The browse branch beside it asked for
+`search.itemsAvailable` at every count, and that key had neither a plural family
+nor a sibling to fall to, so `en` shipped the disagreement in the default
+language; `de`, `es`, `fr` and `pt` read the same way (`1 Elemente verfügbar`,
+`1 elementos disponibles`, `1 éléments disponibles`, `1 itens disponíveis`).
+
+`search.itemsAvailableOne` is the singular half, added to all ten packs, and the
+browse branch now picks between the two on `allItems.length === 1`. That is this
+repo's two-key plural convention — the one `common.itemCount`/`itemCountOne` and
+`detail.reactionCount`/`reactionCountOne` already use — and deliberately not an
+i18next `_one`/`_other` family: full key parity across ten packs caps a family at
+base plus `_one` plus `_other`, so every CLDR category a pack does not spell out
+falls through to the base key, which on this key is the plural. Russian meets
+that at 2 to 4 and Arabic at 2, 3 to 10 and 11 to 99. Choosing the key in the
+component keeps `Intl.PluralRules` and `fallbackLng` out of the path: both halves
+exist in every pack, so no count in any language can reach English.
+
+`zh`, `ja` and `ko` carry the same string in both halves because the counter word
+holds the number and there is no separate singular form; `ru` does too, because
+its phrasing states no noun to agree with.
+
+One translation changed rather than being added: `ar` wrote both numbers into one
+string as a parenthesised marker. Its key can no longer be reached at one item, so
+the singular half of that marker was dead weight while the parentheses still
+rendered at every count the key does serve. It now uses the same noun pair that
+`ar`'s `common.itemCount`/`itemCountOne` already uses.
diff --git a/packages/app-shell/src/views/SearchResultsPage.tsx b/packages/app-shell/src/views/SearchResultsPage.tsx
index e577f47fd2..d20fe3c4ca 100644
--- a/packages/app-shell/src/views/SearchResultsPage.tsx
+++ b/packages/app-shell/src/views/SearchResultsPage.tsx
@@ -219,11 +219,24 @@ export function SearchResultsPage() {
{/* Results count */}
+ {/*
+ Both branches select their key on `=== 1`, and they have to: the browse
+ branch used to ask for `search.itemsAvailable` at every count, so English
+ shipped `1 items available` at a single searchable item (objectui#9664).
+
+ The repo's two-key plural convention (`common.itemCount`/`itemCountOne`,
+ `detail.reactionCount`/`reactionCountOne`), NOT an i18next `_one`/`_other`
+ family. Key parity caps a family at base + `_one` + `_other`, so every
+ other CLDR category falls through to the base key — and on THIS key the
+ base would be the plural, which is what `ar` meets at 2, 3-10 and 11-99
+ and `ru` at 2-4. Picking the key here keeps `Intl.PluralRules` and
+ `fallbackLng` out of the path entirely.
+ */}
{query.trim()
? t(totalCount === 1 ? 'search.resultsCount' : 'search.resultsCountPlural', { count: totalCount, query })
- : t('search.itemsAvailable', { count: allItems.length })}
+ : t(allItems.length === 1 ? 'search.itemsAvailableOne' : 'search.itemsAvailable', { count: allItems.length })}
{recordsSearching && (
({ navigation: [] as unknown[] }));
+/** Every key the component asked `t` for during the last render. */
+const asked = vi.hoisted(() => ({ keys: [] as string[] }));
+/** The `?q=` the page is mounted with; empty means the BROWSE branch. */
+const url = vi.hoisted(() => ({ search: '' }));
+
+vi.mock('react-router-dom', () => ({
+ useParams: () => ({ appName: 'crm' }),
+ useSearchParams: () => [new URLSearchParams(url.search), vi.fn()],
+ Link: ({ to, children, ...rest }: any) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock('@object-ui/i18n', async (importOriginal) => {
+ const actual = await importOriginal();
+ const at = (pack: any, dotted: string) =>
+ dotted.split('.').reduce((node: any, part: string) => node?.[part], pack);
+ return {
+ ...actual,
+ useObjectTranslation: () => ({
+ t: (key: string, options?: Record) => {
+ asked.keys.push(key);
+ // `actual.en` is the shipped catalogue, the same object the app resolves
+ // through `fallbackLng`. Interpolation is i18next's `{{name}}` spelling.
+ const value = at(actual.en, key);
+ if (typeof value !== 'string') return key;
+ return value.replace(/\{\{(\w+)\}\}/g, (_m: string, name: string) =>
+ String(options?.[name] ?? ''),
+ );
+ },
+ }),
+ };
+});
+
+vi.mock('@object-ui/react', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ // Browse mode queries nothing, so the count on screen is the nav count.
+ useRecordSearch: () => ({ results: [], isSearching: false, error: undefined }),
+ };
+});
+
+vi.mock('../../providers/MetadataProvider', () => ({
+ useMetadata: () => ({
+ apps: [{ name: 'crm', label: 'CRM', navigation: app.navigation }],
+ objects: [],
+ }),
+}));
+
+vi.mock('../../providers/AdapterProvider', () => ({
+ useAdapter: () => ({ find: vi.fn(), searchAll: vi.fn() }),
+}));
+
+vi.mock('@object-ui/auth', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useAuth: () => ({ user: { id: 'u1' }, activeOrganization: null }),
+}));
+
+import { SearchResultsPage } from '../SearchResultsPage';
+
+/** `n` object nav items, which is what the page counts as "searchable". */
+const navItems = (n: number) =>
+ Array.from({ length: n }, (_, i) => ({
+ id: `n${i}`,
+ type: 'object',
+ objectName: `crm_object_${i}`,
+ label: `Object ${i}`,
+ }));
+
+describe('SearchResultsPage browse count agrees with its number (objectui#9664)', () => {
+ beforeEach(() => {
+ asked.keys = [];
+ url.search = '';
+ });
+
+ it('reads "1 item available" at exactly one searchable item', () => {
+ app.navigation = navItems(1);
+ render();
+
+ expect(screen.getByText('1 item available')).toBeInTheDocument();
+ // The defect, named as the string it shipped.
+ expect(screen.queryByText('1 items available')).toBeNull();
+ // …and the selection that produces it, so a green here cannot come from a
+ // pack edit that papered over a call site still asking for one key.
+ expect(asked.keys).toContain('search.itemsAvailableOne');
+ expect(asked.keys).not.toContain('search.itemsAvailable');
+ });
+
+ it('reads the plural at every other count, zero included', () => {
+ app.navigation = navItems(4);
+ render();
+
+ expect(screen.getByText('4 items available')).toBeInTheDocument();
+ expect(asked.keys).toContain('search.itemsAvailable');
+ expect(asked.keys).not.toContain('search.itemsAvailableOne');
+ });
+
+ it('leaves the query branch alone — it already switched at one', () => {
+ // The pattern this card copied is on the adjacent line, and the card claims
+ // nothing about it. Measured rather than assumed, so a later edit to the
+ // browse branch cannot quietly take the query branch with it.
+ app.navigation = navItems(3);
+ url.search = 'q=Object 0';
+ render();
+
+ expect(screen.getByText('1 result for "Object 0"')).toBeInTheDocument();
+ expect(asked.keys).toContain('search.resultsCount');
+ expect(asked.keys).not.toContain('search.resultsCountPlural');
+ // The browse branch is not evaluated at all while a query is present.
+ expect(asked.keys).not.toContain('search.itemsAvailable');
+ expect(asked.keys).not.toContain('search.itemsAvailableOne');
+ });
+});
diff --git a/packages/i18n/src/__tests__/searchItemsAvailable-plural-9664.test.ts b/packages/i18n/src/__tests__/searchItemsAvailable-plural-9664.test.ts
new file mode 100644
index 0000000000..9410c5983c
--- /dev/null
+++ b/packages/i18n/src/__tests__/searchItemsAvailable-plural-9664.test.ts
@@ -0,0 +1,212 @@
+/**
+ * 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#9664 — `search.itemsAvailable` broke ENGLISH at one item.
+ *
+ * The full-page search header is one ternary with two branches. The query
+ * branch already picked its key on `totalCount === 1`
+ * (`search.resultsCount`/`search.resultsCountPlural`); the browse branch asked
+ * for `search.itemsAvailable` at every count, so `en`'s `{{count}} items
+ * available` shipped `1 items available` at a single searchable item — in the
+ * default language, with no plural family and no sibling key to fall to.
+ * `de`/`es`/`fr`/`pt` read the same way (`1 Elemente verfügbar`, …).
+ *
+ * ## The repair, and why it is the two-key shape
+ *
+ * `itemsAvailable`/`itemsAvailableOne`, selected at the call site — this repo's
+ * two-key plural convention, the one `common.itemCount`/`itemCountOne`,
+ * `detail.reactionCount`/`reactionCountOne` and
+ * `collaboration.commentCount`/`commentCountOne` already use, and the same
+ * device (in its other spelling) as the `search.resultsCount` pair on the
+ * adjacent branch. ⛔ NOT an i18next `_one`/`_other` family: full key parity
+ * across ten packs caps a family at base + `_one` + `_other`, so every CLDR
+ * category a pack does not spell out falls through to the BASE key — here the
+ * plural — which `ar` meets at 2, 3-10 and 11-99 and `ru` at 2-4. Selecting the
+ * key in the component keeps `Intl.PluralRules` and `fallbackLng` out of the
+ * path: both halves exist in all ten packs, so no count in any language can
+ * reach English.
+ *
+ * ## What this file does NOT own
+ *
+ * That both halves exist in all ten packs and hold `en`'s placeholders is
+ * `all-locales-key-parity.test.ts`'s, and that the call site picks the singular
+ * at one item is `SearchResultsPage.itemsAvailablePlural-9664.test.tsx`'s. This
+ * file owns the VALUES: that the singular half really is singular where the
+ * language has one, that the four packs which repeat the string do so for a
+ * stated reason, and that neither half carries a parenthesised plural marker.
+ */
+import { describe, it, expect } from 'vitest';
+import { builtInLocales } from '../locales';
+
+type LocaleCode = keyof typeof builtInLocales;
+
+const LANGS = Object.keys(builtInLocales) as LocaleCode[];
+
+const at = (pack: unknown, dotted: string) =>
+ dotted.split('.').reduce((n, p) => (n as Record)?.[p], pack);
+
+const KEY = 'search.itemsAvailable';
+const KEY_ONE = 'search.itemsAvailableOne';
+
+/** The count ≠ 1 half, unchanged by this card except in `ar` (see below). */
+const PLURAL: Record = {
+ en: '{{count}} items available',
+ zh: '共 {{count}} 项可搜索',
+ ja: '{{count}} 件利用可能',
+ ko: '{{count}}개 항목 사용 가능',
+ de: '{{count}} Elemente verfügbar',
+ fr: '{{count}} éléments disponibles',
+ es: '{{count}} elementos disponibles',
+ pt: '{{count}} itens disponíveis',
+ ru: '{{count}} доступно',
+ ar: '{{count}} عناصر متاحة',
+};
+
+/** The count === 1 half, added by this card. */
+const ONE: Record = {
+ en: '{{count}} item available',
+ zh: '共 {{count}} 项可搜索',
+ ja: '{{count}} 件利用可能',
+ ko: '{{count}}개 항목 사용 가능',
+ de: '{{count}} Element verfügbar',
+ fr: '{{count}} élément disponible',
+ es: '{{count}} elemento disponible',
+ pt: '{{count}} item disponível',
+ ru: '{{count}} доступно',
+ ar: '{{count}} عنصر متاح',
+};
+
+/**
+ * The four packs whose two halves are deliberately the SAME string, so a later
+ * reader does not "de-duplicate" them back into one key:
+ *
+ * - `zh`/`ja`/`ko` have no separate singular form at all — the counter word
+ * carries the number (共 N 项 / N 件 / N개 항목). This is the same reason
+ * `common.itemCount`/`itemCountOne` repeat in those three packs, and it is
+ * the reason the repo writes count labels as two keys rather than an
+ * i18next family: with a family these packs would want to omit the `_one`
+ * half, and key parity reads a legitimately-absent half as a lost key.
+ * - `ru` phrases the label WITHOUT the noun (`{{count}} доступно`), so it has
+ * no word to agree with the number. That is also why Russian needs nothing
+ * from the `few`/`many` categories on this key.
+ */
+const NO_SINGULAR_FORM: LocaleCode[] = ['zh', 'ja', 'ko', 'ru'];
+
+describe('search.itemsAvailable carries a singular half in every pack (objectui#9664)', () => {
+ it('the walk covers all ten packs — not an empty assertion', () => {
+ expect(LANGS).toHaveLength(10);
+ expect(Object.keys(PLURAL).sort()).toEqual([...LANGS].sort());
+ expect(Object.keys(ONE).sort()).toEqual([...LANGS].sort());
+ });
+
+ it.each(LANGS)('%s defines both halves, with en\'s one hole in each', (lang) => {
+ expect(at(builtInLocales[lang], KEY), `${lang} ${KEY}`).toBe(PLURAL[lang]);
+ expect(at(builtInLocales[lang], KEY_ONE), `${lang} ${KEY_ONE}`).toBe(ONE[lang]);
+ // A half that drops `{{count}}` renders a sentence with the number missing
+ // and no error. Parity compares placeholder SHAPE against `en`; this is the
+ // absolute form, so a pack-wide rewrite cannot satisfy both halves by
+ // agreeing with a broken `en`.
+ for (const value of [PLURAL[lang], ONE[lang]]) {
+ expect(value.match(/\{\{count\}\}/g), `${lang} holes in "${value}"`).toHaveLength(1);
+ }
+ });
+
+ it('the singular half is a DIFFERENT sentence wherever the language has one', () => {
+ const distinguishing = LANGS.filter((l) => !NO_SINGULAR_FORM.includes(l));
+ // The five packs the card measured as plainly wrong at one item, plus `ar`,
+ // which dodged the question with a parenthesised marker instead.
+ expect(distinguishing.sort()).toEqual(['ar', 'de', 'en', 'es', 'fr', 'pt']);
+ for (const lang of distinguishing) {
+ expect(ONE[lang], `${lang} singular is still the plural sentence`).not.toBe(PLURAL[lang]);
+ }
+ for (const lang of NO_SINGULAR_FORM) {
+ expect(ONE[lang], `${lang} halves drifted apart`).toBe(PLURAL[lang]);
+ }
+ });
+
+ it('English at exactly one item reads "1 item available"', () => {
+ // The shipped defect, stated as the string a user saw. `en` is `fallbackLng`
+ // and the default language, so this one is the whole reason the card exists.
+ const render = (value: string, count: number) => value.replace('{{count}}', String(count));
+ expect(render(ONE.en, 1)).toBe('1 item available');
+ expect(render(ONE.en, 1)).not.toBe('1 items available');
+ // …and the plural half still answers every other count, zero included.
+ expect(render(PLURAL.en, 0)).toBe('0 items available');
+ expect(render(PLURAL.en, 2)).toBe('2 items available');
+ });
+
+ it('neither half carries a parenthesised plural marker, in any pack', () => {
+ // `ar` used to write BOTH numbers into one string as `عنصر(عناصر) متاح(ة)`.
+ // That device is the one this repo has already measured and refused
+ // (`marketplace-preview-namespace-3546.test.tsx`, key-scoped to
+ // `preview.history.items`); here it is not merely unidiomatic but dead
+ // weight, because the key it sat on can no longer be reached at one item —
+ // the parentheses would render at every count it DOES serve. The noun pair
+ // it was replaced with is the one `ar.common.itemCount`/`itemCountOne`
+ // already uses.
+ //
+ // The class is Unicode-aware on purpose (objectui#3866): JS `\w` is
+ // [A-Za-z0-9_] with or without `u`, so an ASCII formulation is constant-false
+ // for exactly the non-Latin packs this guard is written to watch.
+ //
+ // ⚠️ The repetition bound is this file's own, and it is WIDER than the
+ // `{1,4}` the sibling pin on `preview.history.items` uses. Measured on the
+ // value this key actually carried: `متاح(ة)` is one letter inside the
+ // parentheses and `عنصر(عناصر)` is five, so a `{1,4}` bound scores the
+ // second marker of the same value as absent. Both counter-examples are
+ // pinned below so the bound cannot be narrowed back without going red.
+ const MARKER = /\([\p{L}]{1,6}\)/u;
+ for (const lang of LANGS) {
+ expect(MARKER.test(PLURAL[lang]), `${lang} ${KEY} has a "(s)" marker`).toBe(false);
+ expect(MARKER.test(ONE[lang]), `${lang} ${KEY_ONE} has a "(s)" marker`).toBe(false);
+ }
+ expect(at(builtInLocales.ar, KEY)).not.toContain('(');
+ // The counter-examples: both markers the retired `ar` value carried score
+ // present under this class, and neither does under the ASCII form.
+ expect(MARKER.test('عنصر(عناصر)')).toBe(true);
+ expect(MARKER.test('متاح(ة)')).toBe(true);
+ expect(/\(\w{1,6}\)/.test('عنصر(عناصر)')).toBe(false);
+ expect(/\(\w{1,6}\)/.test('متاح(ة)')).toBe(false);
+ // …and the narrower bound really is the thing that would have missed one.
+ expect(/\([\p{L}]{1,4}\)/u.test('عنصر(عناصر)')).toBe(false);
+ expect(/\([\p{L}]{1,4}\)/u.test('متاح(ة)')).toBe(true);
+ });
+
+ it('⛔ is NOT an i18next plural family — no pack may grow a suffixed half', () => {
+ // Key parity caps a family at base + `_one` + `_other`; every other category
+ // falls through to the base key, which here is the PLURAL. `ru` would then
+ // read the plural at 2-4 and `ar` at 2, 3-10 and 11-99 — the counts they meet
+ // first. An "upgrade" of this key to a family reintroduces exactly that, so
+ // it fails here instead.
+ for (const lang of LANGS) {
+ for (const suffix of ['_zero', '_one', '_two', '_few', '_many', '_other']) {
+ expect(at(builtInLocales[lang], `${KEY}${suffix}`), `${lang} ${KEY}${suffix}`).toBeUndefined();
+ }
+ }
+ // The categories that make the paragraph above true rather than asserted.
+ expect(new Intl.PluralRules('ru').select(3)).toBe('few');
+ expect(new Intl.PluralRules('ar').select(11)).toBe('many');
+ });
+
+ it('the pattern it mirrors is still on the adjacent branch, untouched', () => {
+ // The premise of the whole repair: the query branch of the same ternary
+ // already switches keys at one, and this card changed nothing about it.
+ for (const lang of LANGS) {
+ expect(at(builtInLocales[lang], 'search.resultsCount'), `${lang} resultsCount`).toEqual(
+ expect.any(String),
+ );
+ expect(
+ at(builtInLocales[lang], 'search.resultsCountPlural'),
+ `${lang} resultsCountPlural`,
+ ).toEqual(expect.any(String));
+ }
+ expect(at(builtInLocales.en, 'search.resultsCount')).toBe('{{count}} result for "{{query}}"');
+ expect(at(builtInLocales.en, 'search.resultsCountPlural')).toBe('{{count}} results for "{{query}}"');
+ });
+});
diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts
index 8306dd1722..15929bbb44 100644
--- a/packages/i18n/src/locales/ar.ts
+++ b/packages/i18n/src/locales/ar.ts
@@ -2490,7 +2490,8 @@ const ar = {
inputAriaLabel: "ابحث في الكائنات، لوحات التحكم، الصفحات، التقارير",
resultsCount: "{{count}} نتيجة لـ \"{{query}}\"",
resultsCountPlural: "{{count}} نتيجة لـ \"{{query}}\"",
- itemsAvailable: "{{count}} عنصر(عناصر) متاح(ة)",
+ itemsAvailable: "{{count}} عناصر متاحة",
+ itemsAvailableOne: "{{count}} عنصر متاح",
noResults: "لم يتم العثور على نتائج",
noResultsHint: "جرب تعديل مصطلحات البحث",
typeObjects: "الكائنات",
diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts
index c08cdd60f9..4a904eec21 100644
--- a/packages/i18n/src/locales/de.ts
+++ b/packages/i18n/src/locales/de.ts
@@ -2484,6 +2484,7 @@ const de = {
resultsCount: "{{count}} Ergebnis für „{{query}}“",
resultsCountPlural: "{{count}} Ergebnisse für „{{query}}“",
itemsAvailable: "{{count}} Elemente verfügbar",
+ itemsAvailableOne: "{{count}} Element verfügbar",
noResults: "Keine Ergebnisse gefunden",
noResultsHint: "Versuchen Sie, Ihre Suchbegriffe anzupassen",
typeObjects: "Objekte",
diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts
index d3ed26d449..f20e20352a 100644
--- a/packages/i18n/src/locales/en.ts
+++ b/packages/i18n/src/locales/en.ts
@@ -2835,6 +2835,7 @@ const en = {
resultsCount: '{{count}} result for "{{query}}"',
resultsCountPlural: '{{count}} results for "{{query}}"',
itemsAvailable: '{{count}} items available',
+ itemsAvailableOne: '{{count}} item available',
noResults: 'No results found',
noResultsHint: 'Try adjusting your search terms',
typeObjects: 'Objects',
diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts
index 52766e6f10..b67fae63ab 100644
--- a/packages/i18n/src/locales/es.ts
+++ b/packages/i18n/src/locales/es.ts
@@ -2488,6 +2488,7 @@ const es = {
resultsCount: "{{count}} resultado para \"{{query}}\"",
resultsCountPlural: "{{count}} resultados para \"{{query}}\"",
itemsAvailable: "{{count}} elementos disponibles",
+ itemsAvailableOne: "{{count}} elemento disponible",
noResults: "Sin resultados encontrados",
noResultsHint: "Intente ajustar sus términos de búsqueda",
typeObjects: "Objetos",
diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts
index d470ac3c8d..d64f5638ac 100644
--- a/packages/i18n/src/locales/fr.ts
+++ b/packages/i18n/src/locales/fr.ts
@@ -2486,6 +2486,7 @@ const fr = {
resultsCount: "{{count}} résultat pour \"{{query}}\"",
resultsCountPlural: "{{count}} résultats pour \"{{query}}\"",
itemsAvailable: "{{count}} éléments disponibles",
+ itemsAvailableOne: "{{count}} élément disponible",
noResults: "Aucun résultat trouvé",
noResultsHint: "Essayez d'ajuster vos termes de recherche",
typeObjects: "Objets",
diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts
index b3fd6097cd..5199ceef9f 100644
--- a/packages/i18n/src/locales/ja.ts
+++ b/packages/i18n/src/locales/ja.ts
@@ -2486,6 +2486,7 @@ const ja = {
resultsCount: "\"{{query}}\" の結果 {{count}} 件",
resultsCountPlural: "\"{{query}}\" の結果 {{count}} 件",
itemsAvailable: "{{count}} 件利用可能",
+ itemsAvailableOne: "{{count}} 件利用可能",
noResults: "結果が見つかりません",
noResultsHint: "検索語句を調整してみてください",
typeObjects: "オブジェクト",
diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts
index 1fca982ea7..67577be735 100644
--- a/packages/i18n/src/locales/ko.ts
+++ b/packages/i18n/src/locales/ko.ts
@@ -2483,6 +2483,7 @@ const ko = {
resultsCount: "\"{{query}}\"에 대한 {{count}}개 결과",
resultsCountPlural: "\"{{query}}\"에 대한 {{count}}개 결과",
itemsAvailable: "{{count}}개 항목 사용 가능",
+ itemsAvailableOne: "{{count}}개 항목 사용 가능",
noResults: "결과를 찾을 수 없습니다",
noResultsHint: "검색어를 조정해 보세요",
typeObjects: "오브젝트",
diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts
index 8784eabf95..f0b15ec16f 100644
--- a/packages/i18n/src/locales/pt.ts
+++ b/packages/i18n/src/locales/pt.ts
@@ -2483,6 +2483,7 @@ const pt = {
resultsCount: "{{count}} resultado para \"{{query}}\"",
resultsCountPlural: "{{count}} resultados para \"{{query}}\"",
itemsAvailable: "{{count}} itens disponíveis",
+ itemsAvailableOne: "{{count}} item disponível",
noResults: "Nenhum resultado encontrado",
noResultsHint: "Tente ajustar seus termos de pesquisa",
typeObjects: "Objetos",
diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts
index 44f2be8098..8bdacb6c2e 100644
--- a/packages/i18n/src/locales/ru.ts
+++ b/packages/i18n/src/locales/ru.ts
@@ -2497,6 +2497,7 @@ const ru = {
resultsCount: "{{count}} результат для \"{{query}}\"",
resultsCountPlural: "{{count}} результатов для \"{{query}}\"",
itemsAvailable: "{{count}} доступно",
+ itemsAvailableOne: "{{count}} доступно",
noResults: "Результатов не найдено",
noResultsHint: "Попробуйте изменить поисковые запросы",
typeObjects: "Объекты",
diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts
index 7c77d25fe4..b68797f23f 100644
--- a/packages/i18n/src/locales/zh.ts
+++ b/packages/i18n/src/locales/zh.ts
@@ -2573,6 +2573,7 @@ const zh = {
resultsCount: '找到 {{count}} 条与“{{query}}”相关的结果',
resultsCountPlural: '找到 {{count}} 条与“{{query}}”相关的结果',
itemsAvailable: '共 {{count}} 项可搜索',
+ itemsAvailableOne: '共 {{count}} 项可搜索',
noResults: '未找到结果',
noResultsHint: '请尝试调整搜索关键字',
typeObjects: '对象',