From b5b604daf249db1845986f519a8e60241004ab4f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 12:32:42 +0000 Subject: [PATCH] fix(app-shell,i18n): browse search count agrees with its number at one item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full-page search header is one ternary with two branches. The query branch already picked its key on `totalCount === 1` (`search.resultsCount` against `search.resultsCountPlural`); the browse branch beside it asked for `search.itemsAvailable` at every count. That key had no plural family and no sibling key, so `en` shipped `1 items available` at a single searchable item, and `de`/`es`/`fr`/`pt` read the same way. Adds `search.itemsAvailableOne` to all ten packs and picks between the two on `allItems.length === 1` — this repo's two-key plural convention (`common.itemCount`/`itemCountOne`, `detail.reactionCount`/`reactionCountOne`), deliberately 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, which here is the plural — what `ru` meets at 2-4 and `ar` at 2, 3-10 and 11-99. Selecting in the component keeps `Intl.PluralRules` and `fallbackLng` out of the path. `zh`/`ja`/`ko` repeat the string (the counter word holds the number) and so does `ru` (its phrasing states no noun). `ar`'s value changed rather than being copied: its parenthesised marker covered both numbers on a key that can no longer be reached at one, so it now uses the noun pair `ar`'s own `common.itemCount` pair already uses. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq --- .../9664-search-items-available-plural.md | 37 +++ .../app-shell/src/views/SearchResultsPage.tsx | 15 +- ...ltsPage.itemsAvailablePlural-9664.test.tsx | 147 ++++++++++++ .../searchItemsAvailable-plural-9664.test.ts | 212 ++++++++++++++++++ packages/i18n/src/locales/ar.ts | 3 +- packages/i18n/src/locales/de.ts | 1 + packages/i18n/src/locales/en.ts | 1 + packages/i18n/src/locales/es.ts | 1 + packages/i18n/src/locales/fr.ts | 1 + packages/i18n/src/locales/ja.ts | 1 + packages/i18n/src/locales/ko.ts | 1 + packages/i18n/src/locales/pt.ts | 1 + packages/i18n/src/locales/ru.ts | 1 + packages/i18n/src/locales/zh.ts | 1 + 14 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 .changeset/9664-search-items-available-plural.md create mode 100644 packages/app-shell/src/views/__tests__/SearchResultsPage.itemsAvailablePlural-9664.test.tsx create mode 100644 packages/i18n/src/__tests__/searchItemsAvailable-plural-9664.test.ts 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: '对象',