From 207b4e99a315ae48361938dad7748a957240da83 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 7 Aug 2026 19:14:07 +0200 Subject: [PATCH 01/19] feat(i18n): drop the 11 non-English locales, prune dead keys English is now the only bundled language. The 11 non-English JSON dictionaries were statically imported and copied into `this.translations` in the Streami18n constructor, making them un-treeshakeable even for integrators who never set `language`. Removes ~122 KB gzip from the ESM bundle (419,172 -> 296,910 bytes, -29%). - delete src/i18n/{de,es,fr,hi,it,ja,ko,nl,pt,ru,tr}.json and translations.ts (this drops the public deTranslations..trTranslations exports) - drop the 11 dayjs locale imports and 11 Dayjs.updateLocale calendar blocks; integrators now import their own dayjs locale and pass dayjsLocaleConfigForLanguage - narrow SupportedTranslations to 'en' - enable removeUnusedKeys, pruning 82 stale keys (706 -> 624). language/* and timestamp/* are preserved explicitly: they resolve from runtime values, so the extractor cannot see them and would otherwise delete them - replace the zero-empty-string validate-translations script with a CI drift gate (git diff --exit-code -- src/i18n after build-translations) - retarget Streami18n tests at registerTranslation, the only path to non-English now, including coverage for partial dictionaries falling back per key --- .github/workflows/ci.yml | 6 +- .lintstagedrc.json | 3 +- i18next.config.ts | 16 +- package.json | 4 +- scripts/validate-translations.js | 34 -- src/i18n/Streami18n.ts | 256 +-------- src/i18n/__tests__/Streami18n.test.ts | 123 ++-- src/i18n/de.json | 708 ----------------------- src/i18n/en.json | 84 +-- src/i18n/es.json | 739 ------------------------ src/i18n/fr.json | 739 ------------------------ src/i18n/hi.json | 709 ----------------------- src/i18n/index.ts | 1 - src/i18n/it.json | 739 ------------------------ src/i18n/ja.json | 690 ----------------------- src/i18n/ko.json | 690 ----------------------- src/i18n/nl.json | 710 ----------------------- src/i18n/pt.json | 739 ------------------------ src/i18n/ru.json | 774 -------------------------- src/i18n/tr.json | 708 ----------------------- src/i18n/translations.ts | 27 - src/i18n/types.ts | 18 +- src/i18n/utils.ts | 15 +- 23 files changed, 116 insertions(+), 8416 deletions(-) delete mode 100644 scripts/validate-translations.js delete mode 100644 src/i18n/de.json delete mode 100644 src/i18n/es.json delete mode 100644 src/i18n/fr.json delete mode 100644 src/i18n/hi.json delete mode 100644 src/i18n/it.json delete mode 100644 src/i18n/ja.json delete mode 100644 src/i18n/ko.json delete mode 100644 src/i18n/nl.json delete mode 100644 src/i18n/pt.json delete mode 100644 src/i18n/ru.json delete mode 100644 src/i18n/tr.json delete mode 100644 src/i18n/translations.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 766d64dce6..56cc947b3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,8 +47,10 @@ jobs: - name: Validate ESM bundle with Node ${{ env.NODE_VERSION }} run: yarn validate-esm - - name: Validate translations - run: yarn validate-translations + - name: Validate translations are in sync with source + # `yarn build` above already ran build-translations; a diff here means the author + # changed a t() call without regenerating src/i18n/en.json. + run: git diff --exit-code -- src/i18n - name: Cache Build Output uses: actions/cache@v5 diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 34404e7c03..64ba1aae64 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,5 +1,4 @@ { "src/**/*.{js,jsx,ts,tsx,md}": "eslint --max-warnings 0 --no-warn-ignored", - "**/*.{js,mjs,ts,mts,jsx,tsx,md,json,yml}": "prettier --list-different", - "src/i18n/*.json": "yarn run validate-translations" + "**/*.{js,mjs,ts,mts,jsx,tsx,md,json,yml}": "prettier --list-different" } diff --git a/i18next.config.ts b/i18next.config.ts index 3cbcc55318..5c62471a8c 100644 --- a/i18next.config.ts +++ b/i18next.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'i18next-cli'; export default defineConfig({ - locales: ['de', 'en', 'es', 'fr', 'hi', 'it', 'ja', 'ko', 'nl', 'pt', 'ru', 'tr'], + locales: ['en'], extract: { defaultNS: false, extractFromComments: false, @@ -11,17 +11,15 @@ export default defineConfig({ keySeparator: false, nsSeparator: false, output: 'src/i18n/{{language}}.json', + // `removeUnusedKeys` prunes anything the extractor cannot see in a `t()` call, so every + // key resolved from a runtime value must be preserved explicitly here or it gets deleted. preservePatterns: [ - // to preserve a whole group + // Integrator-overridable timestamp format strings; never referenced as a literal. 'timestamp/*', - - // or exact key if you want : - // 'timestamp/DateSeparator', - - // or if you’re using explicit namespaces: - // 'translation:timestamp/DateSeparator', + // ISO language names, resolved via `t(languageKey)` in MessageTranslationIndicator. + 'language/*', ], - removeUnusedKeys: false, + removeUnusedKeys: true, }, types: { input: ['locales/{{language}}/{{namespace}}.json'], diff --git a/package.json b/package.json index e5eb958cf7..e2876e7731 100644 --- a/package.json +++ b/package.json @@ -215,7 +215,7 @@ "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css src/plugins/Emojis/styling/index.scss:dist/css/emoji-picker.css src/plugins/ChannelDetail/styling/index.scss:dist/css/channel-detail.css; cp -r src/styling/assets dist/css/assets", "build-translations": "i18next-cli extract", "coverage": "vitest run --coverage", - "lint": "yarn prettier --list-different && yarn eslint && yarn validate-translations", + "lint": "yarn prettier --list-different && yarn eslint", "lint-fix": "yarn prettier-fix && yarn eslint-fix", "eslint": "eslint --max-warnings 0", "eslint-fix": "eslint --fix", @@ -231,7 +231,7 @@ "test:watch": "vitest", "types": "tsc --emitDeclarationOnly false --noEmit", "types:tests": "tsc --project tsconfig.test.json --noEmit", - "validate-translations": "node scripts/validate-translations.js", + "validate-translations": "yarn build-translations && git diff --exit-code -- src/i18n", "validate-cjs": "concurrently 'node scripts/validate-cjs-node-bundle.cjs' 'node scripts/validate-cjs-browser-bundle.cjs'", "validate-esm": "node scripts/validate-esm-node-bundle.mjs", "semantic-release": "semantic-release", diff --git a/scripts/validate-translations.js b/scripts/validate-translations.js deleted file mode 100644 index 02281991bd..0000000000 --- a/scripts/validate-translations.js +++ /dev/null @@ -1,34 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const i18nDirectoryRelativePath = '../src/i18n/'; -const directoryPath = path.join(__dirname, i18nDirectoryRelativePath); -let countMissingTranslations = 0; - -fs.readdir(directoryPath, function (err, files) { - if (err) { - return console.log('Unable to scan directory: ' + err); - } - - files.forEach(function (file) { - if (file.split('.').reverse()[0] !== 'json') return; - // Do whatever you want to do with the file - const data = require(i18nDirectoryRelativePath + file); - const keys = Object.keys(data); - keys.forEach((key) => { - if (!data[key] || data[key] === '') { - countMissingTranslations = countMissingTranslations + 1; - console.error( - '\\033[91m', - 'Missing translation for key "' + key + '" in "' + file + '"', - ); - } - }); - }); - - if (countMissingTranslations > 0) { - process.exitCode = 2; - process.exit(); - } else { - process.exit(0); - } -}); diff --git a/src/i18n/Streami18n.ts b/src/i18n/Streami18n.ts index 0398c4a1d5..e3ac09159b 100644 --- a/src/i18n/Streami18n.ts +++ b/src/i18n/Streami18n.ts @@ -19,35 +19,8 @@ import type { TranslationTopicConstructor } from './TranslationBuilder'; import type { UnknownType } from '../types/types'; import type { CustomFormatters, PredefinedFormatters, TDateTimeParser } from './types'; -import { - deTranslations, - enTranslations, - esTranslations, - frTranslations, - hiTranslations, - itTranslations, - jaTranslations, - koTranslations, - nlTranslations, - ptTranslations, - ruTranslations, - trTranslations, -} from './translations'; - -import 'dayjs/locale/de.js'; -import 'dayjs/locale/es.js'; -import 'dayjs/locale/fr.js'; -import 'dayjs/locale/hi.js'; -import 'dayjs/locale/it.js'; -import 'dayjs/locale/ja.js'; -import 'dayjs/locale/ko.js'; -import 'dayjs/locale/nl.js'; -import 'dayjs/locale/pt.js'; -import 'dayjs/locale/ru.js'; -import 'dayjs/locale/tr.js'; -// These locale imports also set these locale globally. -// So As a last step I am going to import english locale -// to make sure I don't mess up language at other places in app. +import enTranslations from './en.json'; + import 'dayjs/locale/en.js'; const defaultNS = 'translation'; @@ -66,155 +39,6 @@ Dayjs.extend(updateLocale); Dayjs.extend(utc); Dayjs.extend(timezone); -Dayjs.updateLocale('de', { - calendar: { - lastDay: '[gestern um] LT', - lastWeek: '[letzten] dddd [um] LT', - nextDay: '[morgen um] LT', - nextWeek: 'dddd [um] LT', - sameDay: '[heute um] LT', - sameElse: 'L', - }, -}); - -Dayjs.updateLocale('es', { - calendar: { - lastDay: '[ayer a las] LT', - lastWeek: '[pasado] dddd [a] LT', - nextDay: '[mañana a] LT', - nextWeek: 'dddd [a] LT', - sameDay: '[hoy a las] LT', - sameElse: 'L', - }, -}); - -Dayjs.updateLocale('fr', { - calendar: { - lastDay: '[Hier à] LT', - lastWeek: 'dddd [dernier à] LT', - nextDay: '[Demain à] LT', - nextWeek: 'dddd [à] LT', - sameDay: "[Aujourd'hui à] LT", - sameElse: 'L', - }, -}); - -Dayjs.updateLocale('hi', { - calendar: { - lastDay: '[कल] LT', - lastWeek: '[पिछले] dddd, LT', - nextDay: '[कल] LT', - nextWeek: 'dddd, LT', - sameDay: '[आज] LT', - sameElse: 'L', - }, - // Hindi notation for meridiems are quite fuzzy in practice. While there exists - // a rigid notion of a 'Pahar' it is not used as rigidly in modern Hindi. - meridiem(hour: number) { - if (hour < 4) { - return 'रात'; - } else if (hour < 10) { - return 'सुबह'; - } else if (hour < 17) { - return 'दोपहर'; - } else if (hour < 20) { - return 'शाम'; - } else { - return 'रात'; - } - }, - meridiemHour(hour: number, meridiem: string) { - if (hour === 12) { - hour = 0; - } - if (meridiem === 'रात') { - return hour < 4 ? hour : hour + 12; - } else if (meridiem === 'सुबह') { - return hour; - } else if (meridiem === 'दोपहर') { - return hour >= 10 ? hour : hour + 12; - } else if (meridiem === 'शाम') { - return hour + 12; - } - return hour; - }, - meridiemParse: /रात|सुबह|दोपहर|शाम/, -}); - -Dayjs.updateLocale('it', { - calendar: { - lastDay: '[Ieri alle] LT', - lastWeek: '[lo scorso] dddd [alle] LT', - nextDay: '[Domani alle] LT', - nextWeek: 'dddd [alle] LT', - sameDay: '[Oggi alle] LT', - sameElse: 'L', - }, -}); - -Dayjs.updateLocale('ja', { - calendar: { - lastDay: '[昨日] LT', - lastWeek: 'dddd LT', - nextDay: '[明日] LT', - nextWeek: '[次の] dddd LT', - sameDay: '[今日] LT', - sameElse: 'L', - }, -}); - -Dayjs.updateLocale('ko', { - calendar: { - lastDay: '[어제] LT', - lastWeek: '[지난] dddd LT', - nextDay: '[내일] LT', - nextWeek: 'dddd LT', - sameDay: '[오늘] LT', - sameElse: 'L', - }, -}); - -Dayjs.updateLocale('nl', { - calendar: { - lastDay: '[gisteren om] LT', - lastWeek: '[afgelopen] dddd [om] LT', - nextDay: '[morgen om] LT', - nextWeek: 'dddd [om] LT', - sameDay: '[vandaag om] LT', - sameElse: 'L', - }, -}); - -Dayjs.updateLocale('pt', { - calendar: { - lastDay: '[ontem às] LT', - lastWeek: 'dddd [passada às] LT', - nextDay: '[amanhã às] LT', - nextWeek: 'dddd [às] LT', - sameDay: '[hoje às] LT', - sameElse: 'L', - }, -}); - -Dayjs.updateLocale('ru', { - calendar: { - lastDay: '[Вчера, в] LT', - nextDay: '[Завтра, в] LT', - sameDay: '[Сегодня, в] LT', - }, -}); - -Dayjs.updateLocale('tr', { - calendar: { - lastDay: '[dün] LT', - lastWeek: '[geçen] dddd [saat] LT', - nextDay: '[yarın saat] LT', - nextWeek: '[gelecek] dddd [saat] LT', - sameDay: '[bugün saat] LT', - sameElse: 'L', - }, -}); - const en_locale = { formats: {}, months: [ @@ -271,70 +95,43 @@ export type Streami18nOptions = { /** * Wrapper around [i18next](https://www.i18next.com/) class for Stream related i18n. * Instance of this class should be provided to Chat component to handle i18n. - * Stream provides following list of in-built i18n: - * 1. English (en) - * 2. Dutch (nl) - * 3. Russian (ru) - * 4. Turkish (tr) - * 5. French (fr) - * 6. Italian (it) - * 7. Hindi (hi) - * 8. Spanish (es) - * 9. Portuguese (pt) - * 10. German (de) - * 11. Japanese (ja) - * 12. Korean (ko) * - * Simplest way to start using chat components in one of the in-built languages would be following: + * English (`en`) is the only built-in language. Every other language is supplied by the + * integrator via `registerTranslation()` or `translationsForLanguage`. Keys are stable, + * namespaced identifiers (e.g. `messageComposer.sendButton.label`) — see the key reference + * in `src/i18n/en.json`. * - * ``` - * const i18n = new Streami18n({ language 'nl' }); - * - * ... - * - * ``` - * - * If you would like to override certain keys in in-built translation. - * UI will be automatically updated in this case. + * If you would like to override certain keys in the built-in English translation, + * the UI will be automatically updated: * * ``` * const i18n = new Streami18n({ - * language: 'nl', * translationsForLanguage: { - * 'Nothing yet...': 'Nog Niet ...', - * '{{ firstUser }} and {{ secondUser }} are typing...': '{{ firstUser }} en {{ secondUser }} zijn aan het typen...', + * 'messageList.empty': 'Nothing here yet', * } * }); - * - * If you would like to register additional languages, use registerTranslation. You can add as many languages as you want: - * - * i18n.registerTranslation('zh', { - * 'Nothing yet...': 'Nog Niet ...', - * '{{ firstUser }} and {{ secondUser }} are typing...': '{{ firstUser }} en {{ secondUser }} zijn aan het typen...', - * }); - * - * - * ... - * * ``` * - * You can use the same function to add whole new language as well. + * To add a language, use `registerTranslation`. You can add as many as you want: * * ``` - * const i18n = new Streami18n(); + * const i18n = new Streami18n({ language: 'nl' }); * - * i18n.registerTranslation('mr', { - * 'Nothing yet...': 'काहीही नाही ...', - * '{{ firstUser }} and {{ secondUser }} are typing...': '{{ firstUser }} आणि {{ secondUser }} टीपी करत आहेत ', + * i18n.registerTranslation('nl', { + * 'messageList.empty': 'Nog niets...', + * 'typing.multipleUsers': '{{ firstUser }} en {{ secondUser }} zijn aan het typen...', * }); * - * // Make sure to call setLanguage to reflect new language in UI. - * i18n.setLanguage('it'); + * // Make sure to call setLanguage to reflect the new language in the UI. + * i18n.setLanguage('nl'); * * ... * * ``` * + * Keys you do not supply fall back to the English copy that ships inline with each + * component, so a partial dictionary is safe. + * * ## Datetime i18n * * Stream react chat components uses [dayjs](https://day.js.org/en/) internally by default to format datetime stamp. @@ -345,6 +142,10 @@ export type Streami18nOptions = { * Dayjs provides locale config for plenty of languages, you can check the whole list of locale configs at following url * https://github.com/iamkun/dayjs/tree/dev/src/locale * + * Only the `en` dayjs locale is bundled. For any other language you must import the dayjs + * locale yourself (`import 'dayjs/locale/nl.js'`) and/or pass `dayjsLocaleConfigForLanguage`, + * including the `calendar` block — the SDK no longer ships calendar formats for other languages. + * * You can either provide the dayjs locale config while registering * language with Streami18n (either via constructor or registerTranslation()) or you can provide your own Dayjs or Moment instance * to Streami18n constructor, which will be then used internally (using the language locale) in components. @@ -455,18 +256,7 @@ export class Streami18n { [key: string]: typeof enTranslations | UnknownType; }; } = { - de: { [defaultNS]: deTranslations }, en: { [defaultNS]: enTranslations }, - es: { [defaultNS]: esTranslations }, - fr: { [defaultNS]: frTranslations }, - hi: { [defaultNS]: hiTranslations }, - it: { [defaultNS]: itTranslations }, - ja: { [defaultNS]: jaTranslations }, - ko: { [defaultNS]: koTranslations }, - nl: { [defaultNS]: nlTranslations }, - pt: { [defaultNS]: ptTranslations }, - ru: { [defaultNS]: ruTranslations }, - tr: { [defaultNS]: trTranslations }, }; /** diff --git a/src/i18n/__tests__/Streami18n.test.ts b/src/i18n/__tests__/Streami18n.test.ts index db0e3d6335..03dedcbb4a 100644 --- a/src/i18n/__tests__/Streami18n.test.ts +++ b/src/i18n/__tests__/Streami18n.test.ts @@ -5,8 +5,10 @@ import { nanoid } from 'nanoid'; import { default as Dayjs } from 'dayjs'; import moment from 'moment-timezone'; import { fromPartial } from '@total-typescript/shoehorn'; -import { nlTranslations, frTranslations } from '../translations'; +// Only the `en` dayjs locale ships with the SDK; integrators import the ones they need, +// exactly as this test does. import 'dayjs/locale/nl'; +import 'dayjs/locale/fr'; import localeData from 'dayjs/plugin/localeData'; import { NotificationTranslationTopic } from '../TranslationBuilder'; import type { TranslationTopicConstructor } from '../TranslationBuilder'; @@ -88,27 +90,36 @@ describe('Streami18n instance - default', () => { }); }); -describe('Streami18n instance - with built-in langauge', () => { +// `en` is the only bundled language. Non-English support is entirely integrator-supplied, +// so these tests exercise that path rather than deleted built-in dictionaries. +const dutchTranslations = { + 'messageList.empty': 'Nog niets...', + 'messageComposer.sendButton.label': 'Verstuur bericht', +}; + +describe('Streami18n instance - with an integrator-registered language', () => { describe('datetime translations enabled', () => { - const streami18nOptions = { language: 'nl' }; - const streami18n = new Streami18n(streami18nOptions); - it('should provide dutch translator', async () => { - const { t: _t } = await streami18n.getTranslators(); - for (const key in nlTranslations) { - if ( - (key.includes('{{') && key.includes('}}')) || - key.includes('duration/Message reminder') || - key.includes('duration/Remind Me') || - key.includes('duration/Share Location') || - typeof nlTranslations[key] !== 'string' - ) { - continue; - } + const streami18n = new Streami18n({ language: 'nl', logger: () => null }); + streami18n.registerTranslation( + 'nl', + // @ts-expect-error partial translations for testing + dutchTranslations, + ); - expect(_t(key)).toBe(nlTranslations[key]); + it('should translate the registered keys', async () => { + const { t: _t } = await streami18n.getTranslators(); + for (const [key, value] of Object.entries(dutchTranslations)) { + expect(_t(key)).toBe(value); } }); - it('should provide moment with `nl` locale', async () => { + + it('should fall back to the key for unregistered keys', async () => { + const { t: _t } = await streami18n.getTranslators(); + const missing = nanoid(); + expect(_t(missing)).toBe(missing); + }); + + it('should provide dayjs with `nl` locale', async () => { const { tDateTimeParser } = await streami18n.getTranslators(); expect(tDateTimeParser() instanceof Dayjs).toBe(true); expect((tDateTimeParser() as Dayjs.Dayjs).locale()).toBe('nl'); @@ -116,30 +127,25 @@ describe('Streami18n instance - with built-in langauge', () => { }); describe('datetime translations disabled', () => { - const streami18nOptions = { + const streami18n = new Streami18n({ language: 'nl', disableDateTimeTranslations: true, - }; - const streami18n = new Streami18n(streami18nOptions); + logger: () => null, + }); + streami18n.registerTranslation( + 'nl', + // @ts-expect-error partial translations for testing + dutchTranslations, + ); - it('should provide dutch translator', async () => { + it('should translate the registered keys', async () => { const { t: _t } = await streami18n.getTranslators(); - for (const key in nlTranslations) { - if ( - (key.includes('{{') && key.includes('}}')) || - key.includes('duration/Message reminder') || - key.includes('duration/Remind Me') || - key.includes('duration/Share Location') || - typeof nlTranslations[key] !== 'string' - ) { - continue; - } - - expect(_t(key)).toBe(nlTranslations[key]); + for (const [key, value] of Object.entries(dutchTranslations)) { + expect(_t(key)).toBe(value); } }); - it('should provide moment with default `en` locale', async () => { + it('should provide dayjs with default `en` locale', async () => { const { tDateTimeParser } = await streami18n.getTranslators(); expect(tDateTimeParser() instanceof Dayjs).toBe(true); expect((tDateTimeParser() as Dayjs.Dayjs).locale()).toBe('en'); @@ -246,28 +252,43 @@ describe('registerTranslation - register new language `mr` (Marathi) ', () => { }); }); -describe('setLanguage - switch to french', () => { - const streami18nOptions = {}; - const streami18n = new Streami18n(streami18nOptions); +describe('setLanguage - switch to a registered language', () => { + const frenchTranslations = { + 'messageList.empty': 'Rien pour le moment...', + 'messageComposer.sendButton.label': 'Envoyer le message', + }; + + it('should provide the french translator after switching', async () => { + const streami18n = new Streami18n({ logger: () => null }); + streami18n.registerTranslation( + 'fr', + // @ts-expect-error partial translations for testing + frenchTranslations, + ); + + // English before the switch: an unknown key resolves to itself. + const { t: beforeT } = await streami18n.getTranslators(); + expect(beforeT('messageList.empty')).toBe('messageList.empty'); - it('should provide french translator', async () => { await streami18n.setLanguage('fr'); const { t: _t } = await streami18n.getTranslators(); - for (const key in frTranslations) { - if ( - (key.includes('{{') && key.includes('}}')) || - key.includes('duration/Message reminder') || - key.includes('duration/Remind Me') || - key.includes('duration/Share Location') || - typeof nlTranslations[key] !== 'string' - ) { - continue; - } - - expect(_t(key)).toBe(frTranslations[key]); + for (const [key, value] of Object.entries(frenchTranslations)) { + expect(_t(key)).toBe(value); } }); + + it('should fall back to the key for an unregistered language', async () => { + // An unknown language gets an empty dictionary rather than being rejected, so every + // key resolves to itself — which is the inline English default at each call site. + const streami18n = new Streami18n({ language: 'zz', logger: () => null }); + const { t: _t } = await streami18n.getTranslators(); + + expect(streami18n.currentLanguage).toBe('zz'); + expect(_t('messageComposer.sendButton.label')).toBe( + 'messageComposer.sendButton.label', + ); + }); }); describe('Streami18n timezone', () => { diff --git a/src/i18n/de.json b/src/i18n/de.json deleted file mode 100644 index 1c4b76bce5..0000000000 --- a/src/i18n/de.json +++ /dev/null @@ -1,708 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} und {{moreCount}} mehr", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} und {{ lastUser }}", - "{{ count }} files_one": "{{ count }} Datei", - "{{ count }} files_other": "{{ count }} Dateien", - "{{ count }} members_one": "{{ count }} Mitglied", - "{{ count }} members_other": "{{ count }} Mitglieder", - "{{ count }} members added_one": "{{ count }} Mitglied hinzugefügt", - "{{ count }} members added_other": "{{ count }} Mitglieder hinzugefügt", - "{{ count }} people are typing_one": "{{ count }} Person tippt", - "{{ count }} people are typing_many": "{{ count }} Personen tippen", - "{{ count }} people are typing_other": "{{ count }} Personen tippen", - "{{ count }} photos_one": "{{ count }} Foto", - "{{ count }} photos_other": "{{ count }} Fotos", - "{{ count }} reactions_one": "{{ count }} Reaktion", - "{{ count }} reactions_other": "{{ count }} Reaktionen", - "{{ count }} videos_one": "{{ count }} Video", - "{{ count }} videos_other": "{{ count }} Videos", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} und {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }} mehr", - "{{ member }} will be able to message you again.": "{{ member }} kann dir wieder Nachrichten senden.", - "{{ member }} won't be able to message you anymore.": "{{ member }} kann dir keine Nachrichten mehr senden.", - "{{ memberCount }} members": "{{ memberCount }} Mitglieder", - "{{ typing }} are typing": "{{ typing }} tippen", - "{{ typing }} is typing": "{{ typing }} tippt", - "{{ user }} has been muted": "{{ user }} wurde stummgeschaltet", - "{{ user }} has been unmuted": "Die Stummschaltung von {{ user }} wurde aufgehoben", - "{{ user }} is typing...": "{{ user }} tippt...", - "{{ users }} and {{ user }} are typing...": "{{ users }} und {{ user }} tippen...", - "{{ users }} and more are typing...": "{{ users }} und mehr tippen...", - "{{ watcherCount }} online": "{{ watcherCount }} online", - "{{count}} new messages_one": "{{count}} neue Nachricht", - "{{count}} new messages_other": "{{count}} neue Nachrichten", - "{{count}} unread_one": "{{count}} ungelesen", - "{{count}} unread_other": "{{count}} ungelesen", - "{{count}} votes_one": "{{count}} Stimme", - "{{count}} votes_other": "{{count}} Stimmen", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+{{count}} weitere Option", - "+{{count}} more options_other": "+{{count}} weitere Optionen", - "🏙 Attachment...": "🏙 Anhang...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} hat erstellt: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} hat abgestimmt: {{pollOptionText}}", - "📍Shared location": "📍Geteilter Standort", - "Actions": "Actions", - "Add": "Hinzufügen", - "Add {{ count }} members_one": "{{ count }} Mitglied hinzufügen", - "Add {{ count }} members_other": "{{ count }} Mitglieder hinzufügen", - "Add a comment": "Einen Kommentar hinzufügen", - "Add a comment to your poll answer": "Füge einen Kommentar zu deiner Umfrageantwort hinzu", - "Add an option": "Eine Option hinzufügen", - "Add channel members": "Kanalmitglieder hinzufügen", - "Add members": "Mitglieder hinzufügen", - "Add reaction": "Reaktion hinzufügen", - "Admin": "Administrator", - "All results loaded": "Alle Ergebnisse geladen", - "Allow access to camera": "Zugriff auf Kamera erlauben", - "Allow access to microphone": "Zugriff auf Mikrofon erlauben", - "Allow comments": "Kommentare erlauben", - "Allow option suggestion": "Optionsvorschläge erlauben", - "Allow others to add comments": "Anderen das Hinzufügen von Kommentaren erlauben", - "Already a member": "Bereits Mitglied", - "Also send as a direct message": "Auch als Direktnachricht senden", - "Also send in channel": "Auch im Kanal senden", - "Also sent in channel": "Auch im Kanal gesendet", - "An error has occurred during recording": "Ein Fehler ist während der Aufnahme aufgetreten", - "An error has occurred during the recording processing": "Ein Fehler ist während der Aufnahmeverarbeitung aufgetreten", - "Anonymous": "Anonym", - "Anonymous poll": "Anonyme Umfrage", - "Archive": "Archivieren", - "Are you sure you want to delete this message?": "Sind Sie sicher, dass Sie diese Nachricht löschen möchten?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} Anhang", - "aria/{{ count }} attachment_other": "{{ count }} Anhänge", - "aria/{{ count }} search results_one": "{{ count }} Suchergebnis", - "aria/{{ count }} search results_other": "{{ count }} Suchergebnisse", - "aria/{{ count }} suggestions_one": "{{ count }} Vorschlag", - "aria/{{ count }} suggestions_other": "{{ count }} Vorschläge", - "aria/{{ count }} unread message_one": "{{ count }} ungelesene Nachricht", - "aria/{{ count }} unread message_other": "{{ count }} ungelesene Nachrichten", - "aria/{{ setting }} disabled": "{{ setting }} deaktiviert", - "aria/{{ setting }} enabled": "{{ setting }} aktiviert", - "aria/Active": "Aktiv", - "aria/Animated GIF": "Animiertes GIF", - "aria/Animated GIF: {{ title }}": "Animiertes GIF: {{ title }}", - "aria/Attachment": "Anhang", - "aria/Attachment {{ attachmentType }}": "Anhang {{ attachmentType }}", - "aria/Attachment Actions": "Anhangaktionen", - "aria/audio": "Audio", - "aria/Audio position {{ elapsed }} of {{ duration }}": "Audioposition {{ elapsed }} von {{ duration }}", - "aria/Audio position {{ progress }} percent": "Audioposition {{ progress }} Prozent", - "aria/Back to attachments": "Zurück zu Anhängen", - "aria/Back to parent menu button": "Zurück zum übergeordneten Menü Schaltfläche", - "aria/Block User": "Benutzer blockieren", - "aria/Bookmark Message": "Nachricht für später speichern", - "aria/Cancel recording": "Aufnahme abbrechen", - "aria/Cancel Reply": "Antwort abbrechen", - "aria/Channel Actions": "Kanalaktionen", - "aria/Channel details": "Kanaldetails", - "aria/Channel list": "Kanalliste", - "aria/Chat view controls": "Chat-Ansicht-Steuerelemente", - "aria/Chat: {{ channelName }}": "Chat: {{ channelName }}", - "aria/Clear search": "Suche leeren", - "aria/Close callout dialog": "Hinweisdialog schließen", - "aria/Close thread": "Thread schließen", - "aria/Collapse sidebar": "Seitenleiste einklappen", - "aria/Command activated: {{ command }}": "Befehl aktiviert: {{ command }}", - "aria/Command Suggestions": "Befehlsvorschläge", - "aria/Complete recording": "Aufnahme abschließen", - "aria/Copy Message Text": "Nachrichtentext kopieren", - "aria/Decrease value": "Wert verringern", - "aria/Delete Message": "Nachricht löschen", - "aria/Delivered": "Zugestellt", - "aria/Delivery status: {{ deliveryStatus }}": "Zustellstatus: {{ deliveryStatus }}", - "aria/Dismiss notification": "Benachrichtigung schließen", - "aria/Download attachment": "Anhang herunterladen", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "„{{ option }}“ an Position {{ position }} abgelegt.", - "aria/Edit Message": "Nachricht bearbeiten", - "aria/Emoji picker": "Emoji-Auswahl", - "aria/Emoji Suggestions": "Emoji-Vorschläge", - "aria/Exit search": "Suche verlassen", - "aria/Expand sidebar": "Seitenleiste einblenden", - "aria/file": "Datei", - "aria/File upload": "Datei hochladen", - "aria/Flag Message": "Nachricht melden", - "aria/GIF": "GIF", - "aria/Giphy actions": "Giphy-Aktionen", - "aria/Giphy canceled": "Giphy abgebrochen", - "aria/Giphy image changed": "Giphy-Bild geändert", - "aria/Giphy image changed: {{ title }}": "Giphy-Bild geändert: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Giphy-Vorschau, nur für dich sichtbar. Verwende die Aktionen Senden, Mischen oder Abbrechen.", - "aria/Giphy sent": "Giphy gesendet", - "aria/Go back": "Zurück", - "aria/image": "Bild", - "aria/Image failed to load": "Bild konnte nicht geladen werden", - "aria/Increase value": "Wert erhöhen", - "aria/Jump to latest message": "Zur neuesten Nachricht springen", - "aria/Jump to quoted message": "Zur zitierten Nachricht springen", - "aria/Last activity: {{ time }}": "Letzte Aktivität: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "Letzte Nachricht von {{ sender }}: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "Letzte Nachricht: {{ messagePreview }}", - "aria/Mark Message Unread": "Als ungelesen markieren", - "aria/Mark messages as read": "Nachrichten als gelesen markieren", - "aria/Mention Suggestions": "Erwähnungsvorschläge", - "aria/Message Actions": "Nachrichtenaktionen", - "aria/Message from {{ user }},": "Nachricht von {{ user }},", - "aria/Message input": "Nachrichteneingabe", - "aria/Message with attachments": "Nachricht mit Anhängen", - "aria/Message,": "Nachricht,", - "aria/Mute User": "Benutzer stummschalten", - "aria/Next page": "Nächste Seite", - "aria/No search results found": "Keine Suchergebnisse gefunden", - "aria/Notifications": "Benachrichtigungen", - "aria/Open Attachment Selector": "Anhang-Auswahl öffnen", - "aria/Open Channel Actions Menu": "Kanalaktionsmenü öffnen", - "aria/Open channel details": "Kanaldetails öffnen", - "aria/Open channels view": "Kanalansicht öffnen", - "aria/Open image shared by {{ name }}": "Von {{ name }} geteiltes Bild öffnen", - "aria/Open Message Actions Menu": "Nachrichtenaktionsmenü öffnen", - "aria/Open Reaction Selector": "Reaktionsauswahl öffnen", - "aria/Open Thread": "Thread öffnen", - "aria/Open threads view": "Thread-Ansicht öffnen", - "aria/Open threads view with unread threads_one": "Thread-Ansicht öffnen, {{ count }} ungelesener Thread", - "aria/Open threads view with unread threads_other": "Thread-Ansicht öffnen, {{ count }} ungelesene Threads", - "aria/Open video shared by {{ name }}": "Von {{ name }} geteiltes Video öffnen", - "aria/Opened channel: {{ name }}": "Kanal geöffnet: {{ name }}", - "aria/Opened thread in {{ name }}": "Thread in {{ name }} geöffnet", - "aria/Option {{ position }}": "Option {{ position }}", - "aria/Options can now be reordered and removed.": "Optionen können jetzt verschoben und entfernt werden.", - "aria/Pause": "Pausieren", - "aria/Pause recording": "Aufnahme pausieren", - "aria/Percent complete": "{{percent}} Prozent abgeschlossen", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "„{{ option }}“ aufgenommen. Verwenden Sie die Pfeiltasten zum Verschieben. Drücken Sie die Leertaste oder Tab zum Ablegen.", - "aria/Pin Message": "Nachricht anheften", - "aria/Play": "Abspielen", - "aria/Poll dialog opened": "Umfragedialog geöffnet", - "aria/Poll sent": "Umfrage gesendet", - "aria/Poll: {{ pollName }}": "Umfrage: {{ pollName }}", - "aria/Press Enter to start typing": "Drücken Sie die Eingabetaste, um mit der Eingabe zu beginnen", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "Drücken Sie die Leertaste, um diese Option auszuwählen, verwenden Sie die Pfeiltasten nach oben und unten, um sie zu verschieben, und drücken Sie dann erneut die Leertaste, um die Auswahl aufzuheben.", - "aria/Previous page": "Vorherige Seite", - "aria/Quote Message": "Nachricht zitieren", - "aria/Reaction list": "Reaktionsliste", - "aria/Read": "Gelesen", - "aria/Recording paused": "Aufnahme pausiert", - "aria/Recording resumed": "Aufnahme fortgesetzt", - "aria/Recording started": "Aufnahme gestartet", - "aria/Remind Me Message": "Erinnern", - "aria/Remove attachment": "Anhang entfernen", - "aria/Remove location attachment": "Standortanhang entfernen", - "aria/Remove option: {{ option }}": "Option entfernen: {{ option }}", - "aria/Remove Reminder": "Erinnerung entfernen", - "aria/Remove Save For Later": "„Später ansehen“ entfernen", - "aria/Removed option {{ option }}": "Option {{ option }} entfernt", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "„{{ option }}“ neu anordnen, Position {{ position }} von {{ total }}", - "aria/Reorder option {{ position }}": "Option {{ position }} neu anordnen", - "aria/Resend Message": "Nachricht erneut senden", - "aria/Resume recording": "Aufnahme fortsetzen", - "aria/Retry upload": "Upload erneut versuchen", - "aria/Review bounced message": "Zurückgewiesene Nachricht prüfen", - "aria/Search cleared": "Suche gelöscht", - "aria/Search results": "Suchergebnisse", - "aria/Search results header filter button": "Suchergebnisse-Kopfzeilen-Filterbutton", - "aria/Search results header filter button for: {{ source }}": "Filterbutton in der Kopfzeile der Suchergebnisse für: {{ source }}", - "aria/Seek audio position": "Audioposition suchen", - "aria/Select Reaction: {{ reactionName }}": "Reaktion auswählen: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "Benutzerkanal auswählen: {{ name }}", - "aria/Send": "Senden", - "aria/Sent": "Gesendet", - "aria/Shared a link": "Link geteilt", - "aria/Shared a link with title: {{ linkTitle }}": "Link geteilt mit Titel: {{ linkTitle }}", - "aria/Shared location": "Geteilter Standort", - "aria/Show preview": "Vorschau anzeigen", - "aria/Start recording audio": "Audioaufnahme starten", - "aria/Stop AI Generation": "KI-Generierung stoppen", - "aria/Submenu": "Untermenü", - "aria/Suggestions": "Vorschläge", - "aria/There are no messages in this chat.": "In diesem Chat gibt es keine Nachrichten", - "aria/This option can be reordered and removed.": "Diese Option kann verschoben und entfernt werden.", - "aria/Thread list": "Thread-Liste", - "aria/Thread: {{ messagePreview }}": "Thread: {{ messagePreview }}", - "aria/Unblock User": "Benutzer entsperren", - "aria/Unmute User": "Stummschaltung aufheben", - "aria/Unpin Message": "Anheftung aufheben", - "aria/User selected: {{ user }}": "Benutzer ausgewählt: {{ user }}", - "aria/video": "Video", - "aria/voice message": "Sprachnachricht", - "aria/Voice message sent": "Sprachnachricht gesendet", - "aria/Voice recording attached": "Sprachaufnahme angehängt", - "Ask a question": "Eine Frage stellen", - "Attach": "Anhängen", - "Attach files": "Dateien anhängen", - "Attachment": "Anhang", - "Attachment upload blocked due to {{reason}}": "Anhang-Upload blockiert wegen {{reason}}", - "Attachment upload failed due to {{reason}}": "Anhang-Upload fehlgeschlagen wegen {{reason}}", - "Back": "Zurück", - "ban-command-args": "[@Benutzername] [Text]", - "ban-command-description": "Einen Benutzer verbannen", - "Block user": "Benutzer blockieren", - "Block User": "Benutzer blockieren", - "Browse channel members": "Kanalmitglieder durchsuchen", - "Browse pinned messages": "Angeheftete Nachrichten durchsuchen", - "Cancel": "Abbrechen", - "Cannot seek in the recording": "In der Aufnahme kann nicht gesucht werden", - "Changes saved": "Änderungen gespeichert", - "Channel archived": "Kanal archiviert", - "Channel members": "Kanalmitglieder", - "Channel Missing": "Kanal fehlt", - "Channel muted": "Kanal stummgeschaltet", - "Channel pinned": "Kanal angeheftet", - "Channel unarchived": "Kanal dearchiviert", - "Channel unmuted": "Stummschaltung des Kanals aufgehoben", - "Channel unpinned": "Kanal nicht mehr angeheftet", - "Channels": "Kanäle", - "Chat deleted": "Chat deleted", - "Chats": "Chats", - "Choose between 2 to 10 options": "Wähle zwischen 2 und 10 Optionen", - "Close": "Schließen", - "Close dialog": "Dialog schließen", - "Close emoji picker": "Emoji-Auswahl schließen", - "Command not available while editing": "Befehl beim Bearbeiten nicht verfügbar", - "Command not available while replying": "Befehl beim Antworten nicht verfügbar", - "Commands": "Befehle", - "Commands matching": "Übereinstimmende Befehle", - "Connection failure, reconnecting now...": "Verbindungsfehler, Wiederherstellung der Verbindung...", - "Contact info": "Kontaktinfo", - "Contact name": "Kontaktname", - "Copy Message": "Nachricht kopieren", - "Create": "Erstellen", - "Create a question, add options, and configure poll settings": "Erstelle eine Frage, füge Optionen hinzu und konfiguriere die Umfrageeinstellungen", - "Create poll": "Umfrage erstellen", - "Current location": "Aktueller Standort", - "Delete": "Löschen", - "Delete chat": "Chat löschen", - "Delete for me": "Für mich löschen", - "Delete message": "Nachricht löschen", - "Delivered": "Zugestellt", - "Direct message": "Direktnachricht", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "Möchten Sie diese Umfrage jetzt beenden? Niemand wird mehr in dieser Umfrage abstimmen können.", - "Download {{ fileName }}": "{{ fileName }} herunterladen", - "Download All": "Alle herunterladen", - "Download Attachment": "Anhang herunterladen", - "Download attachment {{ name }}": "Anhang {{ name }} herunterladen", - "Download attachment {{ number }}": "Anhang {{ number }} herunterladen", - "Drag your files here": "Ziehen Sie Ihre Dateien hierher", - "Drag your files here to add to your post": "Ziehen Sie Ihre Dateien hierher, um sie Ihrem Beitrag hinzuzufügen", - "Due {{ timeLeft }}": "Fällig {{ timeLeft }}", - "Due since {{ dueSince }}": "Fällig seit {{ dueSince }}", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "Bearbeiten", - "Edit chat data": "Chatdaten bearbeiten", - "Edit contact": "Kontakt bearbeiten", - "Edit group": "Gruppe bearbeiten", - "Edit Message": "Nachricht bearbeiten", - "Edit message request failed": "Anfrage zum Bearbeiten der Nachricht fehlgeschlagen", - "Edited": "Bearbeitet", - "Emoji matching": "Passende Emojis", - "Empty message...": "Leere Nachricht...", - "End": "Beenden", - "End poll": "Umfrage beenden", - "End this poll?": "Umfrage beenden?", - "End vote": "Abstimmung beenden", - "Enforce unique vote is enabled": "Eindeutige Abstimmung ist aktiviert", - "Error": "Fehler", - "Error · Unsent": "Fehler · Nicht gesendet", - "Error adding flag": "Fehler beim Hinzufügen des Flags", - "Error adding members": "Error adding members", - "Error blocking user": "Fehler beim Blockieren des Benutzers", - "Error connecting to chat, refresh the page to try again.": "Verbindungsfehler zum Chat, aktualisieren Sie die Seite, um es erneut zu versuchen.", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "Fehler beim Löschen der Nachricht", - "Error fetching reactions": "Fehler beim Laden von Reaktionen", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "Fehler beim Markieren der Nachricht als ungelesen. Kann keine älteren ungelesenen Nachrichten markieren als die neuesten 100 Kanalnachrichten.", - "Error muting a user ...": "Fehler beim Stummschalten eines Nutzers.", - "Error muting channel": "Fehler beim Stummschalten des Kanals", - "Error muting user": "Fehler beim Stummschalten des Benutzers", - "Error opening direct message": "Fehler beim Öffnen der Direktnachricht", - "Error pinning message": "Fehler beim Pinnen der Nachricht", - "Error removing members": "Fehler beim Entfernen der Mitglieder", - "Error removing message pin": "Fehler beim Entfernen der gepinnten Nachricht", - "Error removing user": "Fehler beim Entfernen des Benutzers", - "Error reproducing the recording": "Fehler bei der Wiedergabe der Aufnahme", - "Error starting recording": "Fehler beim Starten der Aufnahme", - "Error unblocking user": "Fehler beim Entsperren des Benutzers", - "Error unmuting a user ...": "Fehler beim Aufheben der Stummschaltung eines Nutzers ...", - "Error unmuting channel": "Fehler beim Aufheben der Kanal-Stummschaltung", - "Error unmuting user": "Fehler beim Aufheben der Benutzer-Stummschaltung", - "Error uploading attachment": "Fehler beim Hochladen des Anhangs", - "Error uploading file": "Fehler beim Hochladen der Datei", - "Error uploading image": "Fehler beim Hochladen des Bildes", - "Error: {{ errorMessage }}": "Fehler: {{ errorMessage }}", - "Exit command {{ command }}": "Befehl beenden {{ command }}", - "Failed to block user": "Benutzer konnte nicht blockiert werden", - "Failed to create the poll": "Fehler beim Erstellen der Umfrage", - "Failed to create the poll due to {{reason}}": "Die Umfrage konnte aufgrund von {{reason}} nicht erstellt werden", - "Failed to delete the message": "Nachricht konnte nicht gelöscht werden", - "Failed to end the poll": "Umfrage konnte nicht beendet werden", - "Failed to end the poll due to {{reason}}": "Umfrage konnte aufgrund von {{reason}} nicht beendet werden", - "Failed to jump to the first unread message": "Fehler beim Springen zur ersten ungelesenen Nachricht", - "Failed to leave channel": "Kanal konnte nicht verlassen werden", - "Failed to load channels": "Kanäle konnten nicht geladen werden", - "Failed to load more channels": "Weitere Kanäle konnten nicht geladen werden", - "Failed to mark channel as read": "Fehler beim Markieren des Kanals als gelesen", - "Failed to play the recording": "Wiedergabe der Aufnahme fehlgeschlagen", - "Failed to retrieve location": "Standort konnte nicht abgerufen werden", - "Failed to save changes": "Änderungen konnten nicht gespeichert werden", - "Failed to share location": "Standort konnte nicht geteilt werden", - "Failed to update channel archive status": "Archivierungsstatus des Kanals konnte nicht aktualisiert werden", - "Failed to update channel mute status": "Stummschaltungsstatus des Kanals konnte nicht aktualisiert werden", - "Failed to update channel pinned status": "Anheftstatus des Kanals konnte nicht aktualisiert werden", - "File": "Datei", - "File is required for upload attachment": "Datei ist für den Anhang-Upload erforderlich", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "Datei ist zu groß: {{ size }}, maximale Upload-Größe beträgt {{ limit }}", - "File too large": "Datei ist zu groß", - "fileCount_one": "1 datei", - "fileCount_other": "{{ count }} dateien", - "Files": "Dateien", - "Flag": "Melden", - "Generating...": "Generieren...", - "giphy-command-args": "[Text]", - "giphy-command-description": "Poste ein zufälliges Gif in den Kanal", - "Go back": "Zurück", - "Group info": "Gruppeninfo", - "Group name": "Gruppenname", - "Hide who voted": "Verbergen, wer abgestimmt hat", - "Image": "Bild", - "imageCount_one": "Bild", - "imageCount_other": "{{ count }} bilder", - "Instant commands": "Sofortbefehle", - "language/af": "Afrikaans", - "language/am": "Amharisch", - "language/ar": "Arabisch", - "language/az": "Aserbaidschanisch", - "language/bg": "Bulgarisch", - "language/bn": "Bengalisch", - "language/bs": "Bosnisch", - "language/cs": "Tschechisch", - "language/da": "Dänisch", - "language/de": "Deutsch", - "language/el": "Griechisch", - "language/en": "Englisch", - "language/es": "Spanisch", - "language/es-MX": "Spanisch (Mexiko)", - "language/et": "Estnisch", - "language/fa": "Persisch", - "language/fa-AF": "Dari", - "language/fi": "Finnisch", - "language/fr": "Französisch", - "language/fr-CA": "Französisch (Kanada)", - "language/ha": "Hausa", - "language/he": "Hebräisch", - "language/hi": "Hindi", - "language/hr": "Kroatisch", - "language/ht": "Haitianisches Kreolisch", - "language/hu": "Ungarisch", - "language/id": "Indonesisch", - "language/it": "Italienisch", - "language/ja": "Japanisch", - "language/ka": "Georgisch", - "language/ko": "Koreanisch", - "language/lt": "Litauisch", - "language/lv": "Lettisch", - "language/ms": "Malaiisch", - "language/nl": "Niederländisch", - "language/no": "Norwegisch", - "language/pl": "Polnisch", - "language/ps": "Paschtu", - "language/pt": "Portugiesisch", - "language/ro": "Rumänisch", - "language/ru": "Russisch", - "language/sk": "Slowakisch", - "language/sl": "Slowenisch", - "language/so": "Somali", - "language/sq": "Albanisch", - "language/sr": "Serbisch", - "language/sv": "Schwedisch", - "language/sw": "Swahili", - "language/ta": "Tamil", - "language/th": "Thailändisch", - "language/tl": "Tagalog", - "language/tr": "Türkisch", - "language/uk": "Ukrainisch", - "language/ur": "Urdu", - "language/vi": "Vietnamesisch", - "language/zh": "Chinesisch (Vereinfacht)", - "language/zh-TW": "Chinesisch (Traditionell)", - "Last seen {{ timestamp }}": "Zuletzt gesehen {{ timestamp }}", - "Leave Channel": "Kanal verlassen", - "Leave chat": "Kanal verlassen", - "Left channel": "Kanal verlassen", - "Let others add options": "Andere Optionen hinzufügen lassen", - "Limit votes per person": "Stimmen pro Person begrenzen", - "Link": "Link", - "linkCount_one": "Link", - "linkCount_other": "{{ count }} Links", - "live": "live", - "Live for {{duration}}": "Live für {{duration}}", - "Live location": "Live-Standort", - "Live until {{ timestamp }}": "Live bis {{ timestamp }}", - "Load more": "Mehr laden", - "Local upload attachment missing local id": "Lokaler Upload-Anhang hat keine lokale ID", - "Location": "Standort", - "Location sharing ended": "Standortfreigabe beendet", - "Location: {{ coordinates }}": "Standort: {{ coordinates }}", - "Manage channel": "Kanal verwalten", - "Manage members": "Mitglieder verwalten", - "Mark as unread": "Als ungelesen markieren", - "Maximum number of votes (from 2 to 10)": "Maximale Anzahl der Stimmen (von 2 bis 10)", - "Maximum votes per person": "Maximale Stimmen pro Person", - "Member detail": "Mitgliederdetails", - "mention/Channel": "Kanal", - "mention/Channel Description": "Alle in diesem Kanal benachrichtigen", - "mention/Here": "Hier", - "mention/Here Description": "Alle Online-Mitglieder in diesem Kanal benachrichtigen", - "Menu": "Menü", - "Message deleted": "Nachricht gelöscht", - "Message Failed · Click to try again": "Nachricht fehlgeschlagen · Klicken, um es erneut zu versuchen", - "Message Failed · Unauthorized": "Nachricht fehlgeschlagen · Nicht autorisiert", - "Message failed to send": "Nachricht konnte nicht gesendet werden", - "Message has been successfully flagged": "Nachricht wurde erfolgreich gemeldet", - "Message marked as unread": "Nachricht als ungelesen markiert", - "Message pinned": "Nachricht angeheftet", - "Message unpinned": "Nachricht nicht mehr angeheftet", - "Message was blocked by moderation policies": "Nachricht wurde durch moderationsrichtlinien blockiert", - "Messages have been marked unread.": "Nachrichten wurden als ungelesen markiert.", - "Missing permissions to upload the attachment": "Fehlende Berechtigungen zum Hochladen des Anhangs", - "Moderator": "Moderator", - "Multiple votes": "Mehrfachstimmen", - "Mute": "Stummschalten", - "Mute chat": "Chat stummschalten", - "Mute user": "Benutzer stummschalten", - "mute-command-args": "[@Benutzername]", - "mute-command-description": "Stummschalten eines Benutzers", - "network error": "Netzwerkfehler", - "New": "Neu", - "New message from {{user}}": "Neue Nachricht von {{user}}", - "New Messages!": "Neue Nachrichten!", - "Next": "Weiter", - "Next image": "Nächstes Bild", - "No chats here yet…": "Noch keine Chats hier...", - "No conversations yet": "Noch keine Unterhaltungen", - "No files": "Keine Dateien", - "No items exist": "Keine Elemente vorhanden", - "No member found": "Kein Mitglied gefunden", - "No messages found": "Keine Nachrichten gefunden", - "No photos or videos": "Keine Fotos oder Videos", - "No pinned messages": "Keine angehefteten Nachrichten", - "No results found": "Keine Ergebnisse gefunden", - "No user found": "Kein Benutzer gefunden", - "Nobody will be able to vote in this poll anymore.": "Niemand kann mehr in dieser Umfrage abstimmen.", - "Nothing yet...": "Noch nichts...", - "Notify all {{ role }} members": "Alle Mitglieder mit Rolle {{ role }} benachrichtigen", - "Offline": "Offline", - "Ok": "OK", - "Online": "Online", - "Only numbers are allowed": "Nur Zahlen sind erlaubt", - "Only visible to you": "Nur für dich sichtbar", - "Open emoji picker": "Emoji-Auswahl öffnen", - "Open gallery at image {{ index }}": "Galerie bei Bild {{ index }} öffnen", - "Open image in gallery": "Bild in Galerie öffnen", - "Open location in a map": "Standort in einer Karte öffnen", - "Open members actions": "Open members actions", - "Open menu": "Menü öffnen", - "Option already exists": "Option existiert bereits", - "Option is empty": "Option ist leer", - "Options": "Optionen", - "Original": "Original", - "Owner": "Besitzer", - "People matching": "Passende Personen", - "Photo": "Foto", - "Photos & videos": "Fotos & Videos", - "Pin": "Anheften", - "Pin a message to see it here": "Hefte eine Nachricht an, um sie hier zu sehen", - "Pinned by {{ name }}": "Angeheftet von {{ name }}", - "Pinned by You": "Von Ihnen angeheftet", - "Pinned message": "Angeheftete Nachricht", - "Pinned messages": "Angeheftete Nachrichten", - "placeholder/PollComment": "Ihr Kommentar", - "placeholder/PollOptionSuggestion": "Neue Option eingeben", - "Play video": "Video abspielen", - "Playback speed {{ rate }}x": "Wiedergabegeschwindigkeit {{ rate }}x", - "Poll": "Umfrage", - "Poll comments": "Umfragekommentare", - "Poll ended": "Umfrage beendet", - "Poll options": "Umfrageoptionen", - "Poll results": "Umfrageergebnisse", - "Poll sent": "Umfrage gesendet", - "Previous": "Zurück", - "Previous image": "Vorheriges Bild", - "Question": "Frage", - "Question {{ optionOrderNumber}}": "Frage {{ optionOrderNumber}}", - "Question is required": "Frage ist erforderlich", - "Quote Reply": "Zitat-Antwort", - "Reached the vote limit. Remove an existing vote first.": "Das Abstimmungslimit wurde erreicht. Entfernen Sie zuerst eine bestehende Stimme.", - "Recording format is not supported and cannot be reproduced": "Aufnahmeformat wird nicht unterstützt und kann nicht wiedergegeben werden", - "Remind me": "Erinnern", - "Remind Me": "Erinnern", - "Reminder set": "Erinnerung gesetzt", - "Remove": "Entfernen", - "Remove {{ count }} members_one": "{{ count }} Mitglied entfernen", - "Remove {{ count }} members_other": "{{ count }} Mitglieder entfernen", - "Remove {{ member }} from this channel?": "{{ member }} aus diesem Kanal entfernen?", - "Remove channel members": "Kanalmitglieder entfernen", - "Remove reminder": "Erinnerung entfernen", - "Remove save for later": "„Später ansehen“ entfernen", - "Remove user": "Benutzer entfernen", - "Removed {{ count }} members_one": "{{ count }} Mitglied entfernt", - "Removed {{ count }} members_other": "{{ count }} Mitglieder entfernt", - "Replied to a thread": "In einem Thread geantwortet", - "Reply": "Antworten", - "Reply to {{ authorName }}": "Antwort an {{ authorName }}", - "Reply to a message to start a thread": "Antworte auf eine Nachricht, um einen Thread zu starten", - "Reply to Message": "Auf Nachricht antworten", - "replyCount_one": "1 Antwort", - "replyCount_other": "{{ count }} Antworten", - "Resend": "Erneut senden", - "Retry upload": "Upload erneut versuchen", - "Review all options available in this poll": "Überprüfe alle verfügbaren Optionen in dieser Umfrage", - "Review comments submitted with poll answers": "Überprüfe Kommentare, die mit Umfrageantworten eingereicht wurden", - "Review poll results and open an option to see detailed votes": "Überprüfe die Umfrageergebnisse und öffne eine Option, um detaillierte Stimmen zu sehen", - "Review this message and choose whether to delete it, edit it, or send it anyway": "Überprüfe diese Nachricht und wähle, ob du sie löschen, bearbeiten oder trotzdem senden möchtest", - "Review who voted for this option": "Überprüfe, wer für diese Option gestimmt hat", - "Save": "Speichern", - "Save for later": "Für später speichern", - "Saved for later": "Für später gespeichert", - "Search": "Suche", - "Search GIFs": "GIFs suchen", - "search-results-header-filter-source-button-label--channels": "Kanäle", - "search-results-header-filter-source-button-label--messages": "Nachrichten", - "search-results-header-filter-source-button-label--users": "Benutzer", - "Searching for {{ searchSourceType }}...": "Suche nach {{ searchSourceType }}...", - "Searching...": "Suchen...", - "searchResultsCount_one": "1 Ergebnis", - "searchResultsCount_other": "{{ count }} Ergebnisse", - "See all options ({{count}})_one": "Alle Optionen anzeigen ({{count}})", - "See all options ({{count}})_other": "Alle Optionen anzeigen ({{count}})", - "Select a thread to continue the conversation": "Wähle einen Thread aus, um die Unterhaltung fortzusetzen", - "Select more than one option": "Mehr als eine Option auswählen", - "Select one": "Eine auswählen", - "Select one or more": "Eine oder mehrere auswählen", - "Select up to {{count}}_one": "Bis zu {{count}} auswählen", - "Select up to {{count}}_other": "Bis zu {{count}} auswählen", - "Select your current location and optionally enable live location sharing": "Wähle deinen aktuellen Standort und aktiviere optional das Teilen des Live-Standorts", - "Send": "Senden", - "Send a message": "Nachricht senden", - "Send a message to start the conversation": "Senden Sie eine Nachricht, um die Unterhaltung zu beginnen", - "Send Anyway": "Trotzdem senden", - "Send direct message": "Direktnachricht senden", - "Send message request failed": "Senden der Nachrichtenanfrage fehlgeschlagen", - "Send poll": "Umfrage senden", - "Sending...": "Senden...", - "Sent": "Gesendet", - "Share": "Teilen", - "Share a file to see it here": "Teile eine Datei, um sie hier zu sehen", - "Share a photo or video to see it here": "Teile ein Foto oder Video, um es hier zu sehen", - "Share live location for": "Live-Standort teilen für", - "Share Location": "Standort teilen", - "Shared live location": "Geteilter Live-Standort", - "Shared location": "Geteilter Standort", - "Show all": "Alle anzeigen", - "Shuffle": "Mischen", - "size limit": "Größenbeschränkung", - "Slow Mode ON": "Langsamer Modus EIN", - "Slow mode, wait {{ seconds }}s...": "Langsamer Modus, warte {{ seconds }}s...", - "Some of the files will not be accepted": "Einige der Dateien werden nicht akzeptiert", - "Start typing to search": "Tippen Sie, um zu suchen", - "Stop sharing": "Teilen beenden", - "Submit": "Absenden", - "Suggest a new option to add to this poll": "Schlage eine neue Option vor, die zu dieser Umfrage hinzugefügt werden soll", - "Suggest an option": "Eine Option vorschlagen", - "Tap to remove": "Tippen zum Entfernen", - "Tap to remove: {{ reactionName }}": "Tippen zum Entfernen: {{ reactionName }}", - "Thinking...": "Denken...", - "this content could not be displayed": "Dieser Inhalt konnte nicht angezeigt werden", - "This field cannot be empty or contain only spaces": "Dieses Feld darf nicht leer sein oder nur Leerzeichen enthalten", - "This message did not meet our content guidelines": "Diese Nachricht entsprach nicht unseren Inhaltsrichtlinien", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "Dieser Benutzer kann dir wieder Nachrichten senden.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "Thread", - "Thread has not been found": "Thread wurde nicht gefunden", - "Thread reply": "Thread-Antwort", - "Thread Reply": "Thread-Antwort", - "ThreadListUnseenThreadsBanner/loading": "Laden...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} ungelesener Thread", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} ungelesene Threads", - "Threads": "Diskussionen", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Gestern]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Heute]\", \"nextDay\": \"[Morgen]\", \"lastDay\": \"[Gestern]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Letzte] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "vor {{ count }} Tg.", - "timestamp/relativeToday": "Heute", - "timestamp/relativeWeeksAgo": "vor {{ count }} Wo.", - "timestamp/relativeYesterday": "Gestern", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Heute] [um] HH:mm\", \"nextDay\": \"[Morgen] [um] HH:mm\", \"lastDay\": \"[Gestern] [um] HH:mm\", \"nextWeek\": \"dddd [um] HH:mm\", \"lastWeek\": \"[letzten] dddd [um] HH:mm\", \"sameElse\": \"ddd, D MMM [um] HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "Um mit der Aufnahme zu beginnen, erlauben Sie den Zugriff auf die Kamera in Ihrem Browser", - "To start recording, allow the microphone access in your browser": "Um mit der Aufnahme zu beginnen, erlauben Sie den Zugriff auf das Mikrofon in Ihrem Browser", - "totalVoteCount_one": "1 Stimme insgesamt", - "totalVoteCount_other": "{{ count }} Stimmen insgesamt", - "Translated": "Übersetzt", - "Translated from {{ language }}": "Übersetzung aus {{ language }}", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "Geben Sie eine Zahl von 2 bis 10 ein", - "Type your message": "Nachricht eingeben", - "Unarchive": "Archivierung aufheben", - "unban-command-args": "[@Benutzername]", - "unban-command-description": "Einen Benutzer entbannen", - "Unblock": "Entsperren", - "Unblock user": "Benutzer entsperren", - "Unblock User": "Benutzer entsperren", - "unknown error": "Unbekannter Fehler", - "Unmute": "Stummschaltung aufheben", - "Unmute chat": "Chat-Stummschaltung aufheben", - "Unmute user": "Benutzer-Stummschaltung aufheben", - "unmute-command-args": "[@Benutzername]", - "unmute-command-description": "Stummschaltung eines Benutzers aufheben", - "Unpin": "Anheftung aufheben", - "Unread messages": "Ungelesene Nachrichten", - "Unsupported attachment": "Nicht unterstützter Anhang", - "unsupported file type": "Nicht unterstützter Dateityp", - "Update": "Aktualisieren", - "Update the comment attached to your poll answer": "Aktualisiere den Kommentar, der an deine Umfrageantwort angehängt ist", - "Update your comment": "Ihren Kommentar aktualisieren", - "Upload blocked": "Upload blockiert", - "Upload error": "Upload-Fehler", - "Upload failed": "Upload fehlgeschlagen", - "Upload Picture": "Bild hochladen", - "Upload type: \"{{ type }}\" is not allowed": "Upload-Typ: \"{{ type }}\" ist nicht erlaubt", - "User blocked": "Benutzer blockiert", - "User muted": "Benutzer stummgeschaltet", - "User removed": "Benutzer entfernt", - "User unblocked": "Blockierung des Benutzers aufgehoben", - "User unmuted": "Benutzer-Stummschaltung aufgehoben", - "User uploaded content": "Vom Benutzer hochgeladener Inhalt", - "Video": "Video", - "videoCount_one": "Video", - "videoCount_other": "{{ count }} Videos", - "View": "Ansehen", - "View {{count}} comments_one": "{{count}} Kommentar anzeigen", - "View {{count}} comments_other": "{{count}} Kommentare anzeigen", - "View all": "Alle anzeigen", - "View member details for {{ member }}": "Mitgliederdetails für {{ member }} anzeigen", - "View original": "Original anzeigen", - "View results": "Ergebnisse anzeigen", - "View translation": "Übersetzung anzeigen", - "Voice message": "Sprachnachricht", - "Voice message {{ duration }}": "Sprachnachricht {{ duration }}", - "Voice message deleted": "Sprachnachricht gelöscht", - "voiceMessageCount_one": "Sprachnachricht", - "voiceMessageCount_other": "{{ count }} sprachnachrichten", - "Vote ended": "Abstimmung beendet", - "Votes": "Stimmen", - "Wait until all attachments have uploaded": "Bitte warten, bis alle Anhänge hochgeladen wurden", - "Waiting for network…": "Warte auf Netzwerk…", - "You": "Du", - "You have no channels currently": "Du hast momentan noch keine Kanäle", - "You've reached the maximum number of files": "Die maximale Anzahl an Dateien ist erreicht" -} diff --git a/src/i18n/en.json b/src/i18n/en.json index f67d6b8b56..edb58a8931 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -8,7 +8,6 @@ "{{ count }} members added_one": "{{ count }} member added", "{{ count }} members added_other": "{{ count }} members added", "{{ count }} people are typing_one": "{{ count }} person is typing", - "{{ count }} people are typing_many": "{{ count }} people are typing", "{{ count }} people are typing_other": "{{ count }} people are typing", "{{ count }} photos_one": "{{ count }} photo", "{{ count }} photos_other": "{{ count }} photos", @@ -17,7 +16,6 @@ "{{ count }} videos_one": "{{ count }} video", "{{ count }} videos_other": "{{ count }} videos", "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} and {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }} more", "{{ member }} will be able to message you again.": "{{ member }} will be able to message you again.", "{{ member }} won't be able to message you anymore.": "{{ member }} won't be able to message you anymore.", "{{ memberCount }} members": "{{ memberCount }} members", @@ -25,9 +23,6 @@ "{{ typing }} is typing": "{{ typing }} is typing", "{{ user }} has been muted": "{{ user }} has been muted", "{{ user }} has been unmuted": "{{ user }} has been unmuted", - "{{ user }} is typing...": "{{ user }} is typing...", - "{{ users }} and {{ user }} are typing...": "{{ users }} and {{ user }} are typing...", - "{{ users }} and more are typing...": "{{ users }} and more are typing...", "{{ watcherCount }} online": "{{ watcherCount }} online", "{{count}} new messages_one": "{{count}} new message", "{{count}} new messages_other": "{{count}} new messages", @@ -35,7 +30,6 @@ "{{count}} unread_other": "{{count}} unread", "{{count}} votes_one": "{{count}} vote", "{{count}} votes_other": "{{count}} votes", - "+{{ imageCount }}": "+{{ imageCount }}", "+{{count}} more options_one": "+{{count}} more option", "+{{count}} more options_other": "+{{count}} more options", "🏙 Attachment...": "🏙 Attachment...", @@ -56,8 +50,6 @@ "All results loaded": "All results loaded", "Allow access to camera": "Allow access to camera", "Allow access to microphone": "Allow access to microphone", - "Allow comments": "Allow comments", - "Allow option suggestion": "Allow option suggestion", "Allow others to add comments": "Allow Others to Add Comments", "Already a member": "Already a member", "Also send as a direct message": "Also send as a direct message", @@ -105,7 +97,6 @@ "aria/Clear search": "Clear search", "aria/Close callout dialog": "Close callout dialog", "aria/Close thread": "Close thread", - "aria/Collapse sidebar": "Collapse sidebar", "aria/Command activated: {{ command }}": "Command activated: {{ command }}", "aria/Command Suggestions": "Command Suggestions", "aria/Complete recording": "Complete recording", @@ -121,7 +112,6 @@ "aria/Emoji picker": "Emoji picker", "aria/Emoji Suggestions": "Emoji Suggestions", "aria/Exit search": "Exit search", - "aria/Expand sidebar": "Expand sidebar", "aria/file": "file", "aria/File upload": "File upload", "aria/Flag Message": "Flag Message", @@ -132,7 +122,6 @@ "aria/Giphy image changed: {{ title }}": "Giphy image changed: {{ title }}", "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.", "aria/Giphy sent": "Giphy sent", - "aria/Go back": "Go back", "aria/image": "image", "aria/Image failed to load": "Image failed to load", "aria/Increase value": "Increase value", @@ -202,7 +191,6 @@ "aria/Review bounced message": "Review bounced message", "aria/Search cleared": "Search cleared", "aria/Search results": "Search results", - "aria/Search results header filter button": "Search results header filter button", "aria/Search results header filter button for: {{ source }}": "Search results header filter button for: {{ source }}", "aria/Seek audio position": "Seek audio position", "aria/Select Reaction: {{ reactionName }}": "Select Reaction: {{ reactionName }}", @@ -221,7 +209,6 @@ "aria/This option can be reordered and removed.": "This option can be reordered and removed.", "aria/Thread list": "Thread list", "aria/Thread: {{ messagePreview }}": "Thread: {{ messagePreview }}", - "aria/Unblock User": "Unblock User", "aria/Unmute User": "Unmute User", "aria/Unpin Message": "Unpin Message", "aria/User selected: {{ user }}": "User selected: {{ user }}", @@ -232,9 +219,7 @@ "Ask a question": "Ask a Question", "Attach": "Attach", "Attach files": "Attach files", - "Attachment": "Attachment", "Attachment upload blocked due to {{reason}}": "Attachment upload blocked due to {{reason}}", - "Attachment upload failed due to {{reason}}": "Attachment upload failed due to {{reason}}", "Back": "Back", "ban-command-args": "[@username] [text]", "ban-command-description": "Ban a user", @@ -246,7 +231,6 @@ "Cannot seek in the recording": "Cannot seek in the recording", "Changes saved": "Changes saved", "Channel archived": "Channel archived", - "Channel members": "Channel members", "Channel Missing": "Channel Missing", "Channel muted": "Channel muted", "Channel pinned": "Channel pinned", @@ -259,22 +243,17 @@ "Choose between 2 to 10 options": "Choose Between 2 to 10 Options", "Close": "Close", "Close dialog": "Close dialog", - "Close emoji picker": "Close emoji picker", "Command not available while editing": "Command not available while editing", "Command not available while replying": "Command not available while replying", "Commands": "Commands", - "Commands matching": "Commands matching", - "Connection failure, reconnecting now...": "Connection failure, reconnecting now...", "Contact info": "Contact info", "Contact name": "Contact name", "Copy Message": "Copy Message", - "Create": "Create", "Create a question, add options, and configure poll settings": "Create a question, add options, and configure poll settings", "Create poll": "Create Poll", "Current location": "Current location", "Delete": "Delete", "Delete chat": "Delete chat", - "Delete for me": "Delete for me", "Delete message": "Delete message", "Delivered": "Delivered", "Direct message": "Direct message", @@ -282,10 +261,8 @@ "Download {{ fileName }}": "Download {{ fileName }}", "Download All": "Download All", "Download Attachment": "Download Attachment", - "Download attachment {{ name }}": "Download attachment {{ name }}", "Download attachment {{ number }}": "Download attachment {{ number }}", "Drag your files here": "Drag your files here", - "Drag your files here to add to your post": "Drag your files here to add to your post", "Due {{ timeLeft }}": "Due {{ timeLeft }}", "Due since {{ dueSince }}": "Due since {{ dueSince }}", "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", @@ -298,19 +275,14 @@ "Edit Message": "Edit Message", "Edit message request failed": "Edit message request failed", "Edited": "Edited", - "Emoji matching": "Emoji matching", "Empty message...": "Empty message...", - "End": "End", "End poll": "End Poll", "End this poll?": "End this Poll?", - "End vote": "End Vote", "Enforce unique vote is enabled": "Enforce unique vote is enabled", "Error": "Error", - "Error · Unsent": "Error · Unsent", "Error adding flag": "Error adding flag", "Error adding members": "Error adding members", "Error blocking user": "Error blocking user", - "Error connecting to chat, refresh the page to try again.": "Error connecting to chat, refresh the page to try again.", "Error deleting chat": "Error deleting chat", "Error deleting message": "Error deleting message", "Error fetching reactions": "Error loading reactions", @@ -320,7 +292,6 @@ "Error muting user": "Error muting user", "Error opening direct message": "Error opening direct message", "Error pinning message": "Error pinning message", - "Error removing members": "Error removing members", "Error removing message pin": "Error removing message pin", "Error removing user": "Error removing user", "Error reproducing the recording": "Error reproducing the recording", @@ -329,22 +300,12 @@ "Error unmuting a user ...": "Error unmuting a user ...", "Error unmuting channel": "Error unmuting channel", "Error unmuting user": "Error unmuting user", - "Error uploading attachment": "Error uploading attachment", - "Error uploading file": "Error uploading file", - "Error uploading image": "Error uploading image", "Error: {{ errorMessage }}": "Error: {{ errorMessage }}", "Exit command {{ command }}": "Exit command {{ command }}", "Failed to block user": "Failed to block user", - "Failed to create the poll": "Failed to create the poll", - "Failed to create the poll due to {{reason}}": "Failed to create the poll due to {{reason}}", - "Failed to delete the message": "Failed to delete the message", "Failed to end the poll": "Failed to end the poll", - "Failed to end the poll due to {{reason}}": "Failed to end the poll due to {{reason}}", "Failed to jump to the first unread message": "Failed to jump to the first unread message", "Failed to leave channel": "Failed to leave channel", - "Failed to load channels": "Failed to load channels", - "Failed to load more channels": "Failed to load more channels", - "Failed to mark channel as read": "Failed to mark channel as read", "Failed to play the recording": "Failed to play the recording", "Failed to retrieve location": "Failed to retrieve location", "Failed to save changes": "Failed to save changes", @@ -354,7 +315,6 @@ "Failed to update channel pinned status": "Failed to update channel pinned status", "File": "File", "File is required for upload attachment": "File is required for upload attachment", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "File is too large: {{ size }}, maximum upload size is {{ limit }}", "File too large": "File too large", "fileCount_one": "File", "fileCount_other": "{{ count }} files", @@ -367,7 +327,6 @@ "Group info": "Group info", "Group name": "Group name", "Hide who voted": "Hide Who Voted", - "Image": "Image", "imageCount_one": "Image", "imageCount_other": "{{ count }} images", "Instant commands": "Instant commands", @@ -434,10 +393,8 @@ "Left channel": "Left channel", "Let others add options": "Let Others Add Options", "Limit votes per person": "Limit Votes per Person", - "Link": "Link", "linkCount_one": "Link", "linkCount_other": "{{ count }} links", - "live": "live", "Live for {{duration}}": "Live for {{duration}}", "Live location": "Live location", "Live until {{ timestamp }}": "Live until {{ timestamp }}", @@ -447,27 +404,18 @@ "Location sharing ended": "Location sharing ended", "Location: {{ coordinates }}": "Location: {{ coordinates }}", "Manage channel": "Manage channel", - "Manage members": "Manage members", "Mark as unread": "Mark as unread", - "Maximum number of votes (from 2 to 10)": "Maximum number of votes (from 2 to 10)", "Maximum votes per person": "Maximum votes per person", "Member detail": "Member detail", - "mention/Channel": "Channel", "mention/Channel Description": "Notify everyone in this channel", - "mention/Here": "Here", "mention/Here Description": "Notify every online member in this channel", - "Menu": "Menu", "Message deleted": "Message deleted", - "Message Failed · Click to try again": "Message Failed · Click to try again", - "Message Failed · Unauthorized": "Message Failed · Unauthorized", "Message failed to send": "Message failed to send", "Message has been successfully flagged": "Message has been successfully flagged", "Message marked as unread": "Message marked as unread", "Message pinned": "Message pinned", "Message unpinned": "Message unpinned", "Message was blocked by moderation policies": "Message was blocked by moderation policies", - "Messages have been marked unread.": "Messages have been marked unread.", - "Missing permissions to upload the attachment": "Missing permissions to upload the attachment", "Moderator": "Moderator", "Multiple votes": "Multiple Votes", "Mute": "Mute", @@ -475,13 +423,10 @@ "Mute user": "Mute user", "mute-command-args": "[@username]", "mute-command-description": "Mute a user", - "network error": "network error", - "New": "New", "New message from {{user}}": "New message from {{user}}", "New Messages!": "New Messages!", "Next": "Next", "Next image": "Next image", - "No chats here yet…": "No chats here yet…", "No conversations yet": "No conversations yet", "No files": "No files", "No items exist": "No items exist", @@ -491,15 +436,12 @@ "No pinned messages": "No pinned messages", "No results found": "No results found", "No user found": "No user found", - "Nobody will be able to vote in this poll anymore.": "Nobody will be able to vote in this poll anymore.", "Nothing yet...": "Nothing yet...", "Notify all {{ role }} members": "Notify all {{ role }} members", "Offline": "Offline", - "Ok": "Ok", "Online": "Online", "Only numbers are allowed": "Only numbers are allowed", "Only visible to you": "Only visible to you", - "Open emoji picker": "Open emoji picker", "Open gallery at image {{ index }}": "Open gallery at image {{ index }}", "Open image in gallery": "Open image in gallery", "Open location in a map": "Open location in a map", @@ -510,7 +452,6 @@ "Options": "Options", "Original": "Original", "Owner": "Owner", - "People matching": "People matching", "Photo": "Photo", "Photos & videos": "Photos & videos", "Pin": "Pin", @@ -540,21 +481,14 @@ "Remind me": "Remind me", "Remind Me": "Remind Me", "Reminder set": "Reminder set", - "Remove": "Remove", - "Remove {{ count }} members_one": "Remove {{ count }} member", - "Remove {{ count }} members_other": "Remove {{ count }} members", "Remove {{ member }} from this channel?": "Remove {{ member }} from this channel?", - "Remove channel members": "Remove channel members", "Remove reminder": "Remove reminder", "Remove save for later": "Remove save for later", "Remove user": "Remove user", - "Removed {{ count }} members_one": "Removed {{ count }} member", - "Removed {{ count }} members_other": "Removed {{ count }} members", "Replied to a thread": "Replied to a thread", "Reply": "Reply", "Reply to {{ authorName }}": "Reply to {{ authorName }}", "Reply to a message to start a thread": "Reply to a message to start a thread", - "Reply to Message": "Reply to Message", "replyCount_one": "1 reply", "replyCount_other": "{{ count }} replies", "Resend": "Resend", @@ -573,12 +507,6 @@ "search-results-header-filter-source-button-label--messages": "messages", "search-results-header-filter-source-button-label--users": "users", "Searching for {{ searchSourceType }}...": "Searching for {{ searchSourceType }}...", - "Searching...": "Searching...", - "searchResultsCount_one": "1 result", - "searchResultsCount_other": "{{ count }} results", - "See all options ({{count}})_one": "See all options ({{count}})", - "See all options ({{count}})_other": "See all options ({{count}})", - "Select a thread to continue the conversation": "Select a thread to continue the conversation", "Select more than one option": "Select More Than One Option", "Select one": "Select one", "Select one or more": "Select one or more", @@ -587,7 +515,6 @@ "Select your current location and optionally enable live location sharing": "Select your current location and optionally enable live location sharing", "Send": "Send", "Send a message": "Send a message", - "Send a message to start the conversation": "Send a message to start the conversation", "Send Anyway": "Send Anyway", "Send direct message": "Send direct message", "Send message request failed": "Send message request failed", @@ -599,17 +526,13 @@ "Share a photo or video to see it here": "Share a photo or video to see it here", "Share live location for": "Share live location for", "Share Location": "Share Location", - "Shared live location": "Shared live location", "Shared location": "Shared location", - "Show all": "Show all", "Shuffle": "Shuffle", "size limit": "size limit", - "Slow Mode ON": "Slow Mode ON", "Slow mode, wait {{ seconds }}s...": "Slow mode, wait {{ seconds }}s...", "Some of the files will not be accepted": "Some of the files will not be accepted", "Start typing to search": "Start typing to search", "Stop sharing": "Stop sharing", - "Submit": "Submit", "Suggest a new option to add to this poll": "Suggest a new option to add to this poll", "Suggest an option": "Suggest an Option", "Tap to remove": "Tap to remove", @@ -623,7 +546,6 @@ "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", "Thread": "Thread", "Thread has not been found": "Thread has not been found", - "Thread reply": "Thread reply", "Thread Reply": "Thread Reply", "ThreadListUnseenThreadsBanner/loading": "Loading...", "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} unread thread", @@ -651,7 +573,6 @@ "Translated from {{ language }}": "Translated from {{ language }}", "translationBuilderTopic/notification": "{{value, notification}}", "Type a number from 2 to 10": "Type a number from 2 to 10", - "Type your message": "Type your message", "Unarchive": "Unarchive", "unban-command-args": "[@username]", "unban-command-description": "Unban a user", @@ -675,7 +596,6 @@ "Upload error": "Upload error", "Upload failed": "Upload failed", "Upload Picture": "Upload Picture", - "Upload type: \"{{ type }}\" is not allowed": "Upload type: \"{{ type }}\" is not allowed", "User blocked": "User blocked", "User muted": "User muted", "User removed": "User removed", @@ -702,7 +622,5 @@ "Votes": "Votes", "Wait until all attachments have uploaded": "Wait until all attachments have uploaded", "Waiting for network…": "Waiting for network…", - "You": "You", - "You have no channels currently": "You have no channels currently", - "You've reached the maximum number of files": "You've reached the maximum number of files" + "You": "You" } diff --git a/src/i18n/es.json b/src/i18n/es.json deleted file mode 100644 index f17a7495c0..0000000000 --- a/src/i18n/es.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} y {{ moreCount }} más", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} y {{ lastUser }}", - "{{ count }} files_one": "{{ count }} archivo", - "{{ count }} files_many": "{{ count }} archivos", - "{{ count }} files_other": "{{ count }} archivos", - "{{ count }} members_one": "{{ count }} miembro", - "{{ count }} members_many": "{{ count }} miembros", - "{{ count }} members_other": "{{ count }} miembros", - "{{ count }} members added_one": "{{ count }} miembro añadido", - "{{ count }} members added_many": "{{ count }} miembros añadidos", - "{{ count }} members added_other": "{{ count }} miembros añadidos", - "{{ count }} people are typing_one": "{{ count }} persona está escribiendo", - "{{ count }} people are typing_many": "{{ count }} personas están escribiendo", - "{{ count }} people are typing_other": "{{ count }} personas están escribiendo", - "{{ count }} photos_one": "{{ count }} foto", - "{{ count }} photos_many": "{{ count }} fotos", - "{{ count }} photos_other": "{{ count }} fotos", - "{{ count }} reactions_one": "{{ count }} reacción", - "{{ count }} reactions_many": "{{ count }} reacciones", - "{{ count }} reactions_other": "{{ count }} reacciones", - "{{ count }} videos_one": "{{ count }} vídeo", - "{{ count }} videos_many": "{{ count }} vídeos", - "{{ count }} videos_other": "{{ count }} vídeos", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} y {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }} más", - "{{ member }} will be able to message you again.": "{{ member }} podrá enviarte mensajes de nuevo.", - "{{ member }} won't be able to message you anymore.": "{{ member }} ya no podrá enviarte mensajes.", - "{{ memberCount }} members": "{{ memberCount }} miembros", - "{{ typing }} are typing": "{{ typing }} están escribiendo", - "{{ typing }} is typing": "{{ typing }} está escribiendo", - "{{ user }} has been muted": "{{ user }} ha sido silenciado", - "{{ user }} has been unmuted": "Se ha desactivado el silencio de {{ user }}", - "{{ user }} is typing...": "{{ user }} está escribiendo...", - "{{ users }} and {{ user }} are typing...": "{{ users }} y {{ user }} están escribiendo...", - "{{ users }} and more are typing...": "{{ users }} y más están escribiendo...", - "{{ watcherCount }} online": "{{ watcherCount }} en línea", - "{{count}} new messages_one": "{{count}} nuevo mensaje", - "{{count}} new messages_many": "{{count}} nuevos mensajes", - "{{count}} new messages_other": "{{count}} nuevos mensajes", - "{{count}} unread_one": "{{count}} no leído", - "{{count}} unread_many": "{{count}} no leídos", - "{{count}} unread_other": "{{count}} no leídos", - "{{count}} votes_one": "1 voto", - "{{count}} votes_many": "{{count}} votos", - "{{count}} votes_other": "{{count}} votos", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+{{count}} opción más", - "+{{count}} more options_many": "+{{count}} opciones más", - "+{{count}} more options_other": "+{{count}} opciones más", - "🏙 Attachment...": "🏙 Adjunto...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} creó: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} votó: {{pollOptionText}}", - "📍Shared location": "📍Ubicación compartida", - "Actions": "Actions", - "Add": "Añadir", - "Add {{ count }} members_one": "Añadir {{ count }} miembro", - "Add {{ count }} members_many": "Añadir {{ count }} miembros", - "Add {{ count }} members_other": "Añadir {{ count }} miembros", - "Add a comment": "Agregar un comentario", - "Add a comment to your poll answer": "Añade un comentario a tu respuesta de la encuesta", - "Add an option": "Agregar una opción", - "Add channel members": "Añadir miembros al canal", - "Add members": "Añadir miembros", - "Add reaction": "Añadir reacción", - "Admin": "Administrador", - "All results loaded": "Todos los resultados cargados", - "Allow access to camera": "Permitir acceso a la cámara", - "Allow access to microphone": "Permitir acceso al micrófono", - "Allow comments": "Permitir comentarios", - "Allow option suggestion": "Permitir sugerencia de opciones", - "Allow others to add comments": "Permitir que otros añadan comentarios", - "Already a member": "Ya es miembro", - "Also send as a direct message": "También enviar como mensaje directo", - "Also send in channel": "También enviar en el canal", - "Also sent in channel": "También enviado en el canal", - "An error has occurred during recording": "Se ha producido un error durante la grabación", - "An error has occurred during the recording processing": "Se ha producido un error durante el procesamiento de la grabación", - "Anonymous": "Anónimo", - "Anonymous poll": "Encuesta anónima", - "Archive": "Archivo", - "Are you sure you want to delete this message?": "¿Estás seguro de que quieres eliminar este mensaje?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_many": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} adjunto", - "aria/{{ count }} attachment_many": "{{ count }} adjuntos", - "aria/{{ count }} attachment_other": "{{ count }} adjuntos", - "aria/{{ count }} search results_one": "{{ count }} resultado de búsqueda", - "aria/{{ count }} search results_many": "{{ count }} resultados de búsqueda", - "aria/{{ count }} search results_other": "{{ count }} resultados de búsqueda", - "aria/{{ count }} suggestions_one": "{{ count }} sugerencia", - "aria/{{ count }} suggestions_many": "{{ count }} sugerencias", - "aria/{{ count }} suggestions_other": "{{ count }} sugerencias", - "aria/{{ count }} unread message_one": "{{ count }} mensaje sin leer", - "aria/{{ count }} unread message_many": "{{ count }} mensajes sin leer", - "aria/{{ count }} unread message_other": "{{ count }} mensajes sin leer", - "aria/{{ setting }} disabled": "{{ setting }} desactivado", - "aria/{{ setting }} enabled": "{{ setting }} activado", - "aria/Active": "Activo", - "aria/Animated GIF": "GIF animado", - "aria/Animated GIF: {{ title }}": "GIF animado: {{ title }}", - "aria/Attachment": "Adjunto", - "aria/Attachment {{ attachmentType }}": "Adjunto {{ attachmentType }}", - "aria/Attachment Actions": "Acciones del adjunto", - "aria/audio": "audio", - "aria/Audio position {{ elapsed }} of {{ duration }}": "Posición de audio {{ elapsed }} de {{ duration }}", - "aria/Audio position {{ progress }} percent": "Posición de audio {{ progress }} por ciento", - "aria/Back to attachments": "Volver a adjuntos", - "aria/Back to parent menu button": "Volver al menú superior botón", - "aria/Block User": "Bloquear usuario", - "aria/Bookmark Message": "Guardar mensaje", - "aria/Cancel recording": "Cancelar grabación", - "aria/Cancel Reply": "Cancelar respuesta", - "aria/Channel Actions": "Acciones del canal", - "aria/Channel details": "Detalles del canal", - "aria/Channel list": "Lista de canales", - "aria/Chat view controls": "Controles de la vista del chat", - "aria/Chat: {{ channelName }}": "Chat: {{ channelName }}", - "aria/Clear search": "Borrar búsqueda", - "aria/Close callout dialog": "Cerrar diálogo de aviso", - "aria/Close thread": "Cerrar hilo", - "aria/Collapse sidebar": "Contraer barra lateral", - "aria/Command activated: {{ command }}": "Comando activado: {{ command }}", - "aria/Command Suggestions": "Sugerencias de comandos", - "aria/Complete recording": "Completar grabación", - "aria/Copy Message Text": "Copiar texto del mensaje", - "aria/Decrease value": "Disminuir valor", - "aria/Delete Message": "Eliminar mensaje", - "aria/Delivered": "Entregado", - "aria/Delivery status: {{ deliveryStatus }}": "Estado de entrega: {{ deliveryStatus }}", - "aria/Dismiss notification": "Descartar notificación", - "aria/Download attachment": "Descargar adjunto", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "«{{ option }}» soltada en la posición {{ position }}.", - "aria/Edit Message": "Editar mensaje", - "aria/Emoji picker": "Selector de emojis", - "aria/Emoji Suggestions": "Sugerencias de emojis", - "aria/Exit search": "Salir de la búsqueda", - "aria/Expand sidebar": "Expandir barra lateral", - "aria/file": "archivo", - "aria/File upload": "Carga de archivo", - "aria/Flag Message": "Marcar mensaje", - "aria/GIF": "GIF", - "aria/Giphy actions": "Acciones de Giphy", - "aria/Giphy canceled": "Giphy cancelado", - "aria/Giphy image changed": "Imagen de Giphy cambiada", - "aria/Giphy image changed: {{ title }}": "Imagen de Giphy cambiada: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Vista previa de Giphy, solo visible para ti. Usa las acciones Enviar, Mezclar o Cancelar.", - "aria/Giphy sent": "Giphy enviado", - "aria/Go back": "Volver", - "aria/image": "imagen", - "aria/Image failed to load": "Error al cargar la imagen", - "aria/Increase value": "Aumentar valor", - "aria/Jump to latest message": "Ir al mensaje más reciente", - "aria/Jump to quoted message": "Ir al mensaje citado", - "aria/Last activity: {{ time }}": "Última actividad: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "Último mensaje de {{ sender }}: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "Último mensaje: {{ messagePreview }}", - "aria/Mark Message Unread": "Marcar como no leído", - "aria/Mark messages as read": "Marcar mensajes como leídos", - "aria/Mention Suggestions": "Sugerencias de menciones", - "aria/Message Actions": "Acciones del mensaje", - "aria/Message from {{ user }},": "Mensaje de {{ user }},", - "aria/Message input": "Entrada de mensaje", - "aria/Message with attachments": "Mensaje con adjuntos", - "aria/Message,": "Mensaje,", - "aria/Mute User": "Silenciar usuario", - "aria/Next page": "Página siguiente", - "aria/No search results found": "No se encontraron resultados de búsqueda", - "aria/Notifications": "Notificaciones", - "aria/Open Attachment Selector": "Abrir selector de adjuntos", - "aria/Open Channel Actions Menu": "Abrir menú de acciones del canal", - "aria/Open channel details": "Abrir detalles del canal", - "aria/Open channels view": "Abrir vista de canales", - "aria/Open image shared by {{ name }}": "Abrir imagen compartida por {{ name }}", - "aria/Open Message Actions Menu": "Abrir menú de acciones de mensaje", - "aria/Open Reaction Selector": "Abrir selector de reacciones", - "aria/Open Thread": "Abrir hilo", - "aria/Open threads view": "Abrir vista de hilos", - "aria/Open threads view with unread threads_one": "Abrir vista de hilos, {{ count }} hilo no leído", - "aria/Open threads view with unread threads_many": "Abrir vista de hilos, {{ count }} hilos no leídos", - "aria/Open threads view with unread threads_other": "Abrir vista de hilos, {{ count }} hilos no leídos", - "aria/Open video shared by {{ name }}": "Abrir video compartido por {{ name }}", - "aria/Opened channel: {{ name }}": "Canal abierto: {{ name }}", - "aria/Opened thread in {{ name }}": "Hilo abierto en {{ name }}", - "aria/Option {{ position }}": "Opción {{ position }}", - "aria/Options can now be reordered and removed.": "Ahora las opciones se pueden reordenar y eliminar.", - "aria/Pause": "Pausar", - "aria/Pause recording": "Pausar grabación", - "aria/Percent complete": "{{percent}} por ciento completado", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "Has cogido «{{ option }}». Usa las flechas para reordenar. Pulsa Espacio o Tab para soltarla.", - "aria/Pin Message": "Fijar mensaje", - "aria/Play": "Reproducir", - "aria/Poll dialog opened": "Cuadro de diálogo de encuesta abierto", - "aria/Poll sent": "Encuesta enviada", - "aria/Poll: {{ pollName }}": "Encuesta: {{ pollName }}", - "aria/Press Enter to start typing": "Pulsa Intro para empezar a escribir", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "Pulsa la barra espaciadora para seleccionar esta opción, usa las flechas Arriba y Abajo para moverla y, después, vuelve a pulsar la barra espaciadora para deseleccionarla.", - "aria/Previous page": "Página anterior", - "aria/Quote Message": "Citar mensaje", - "aria/Reaction list": "Lista de reacciones", - "aria/Read": "Leído", - "aria/Recording paused": "Grabación pausada", - "aria/Recording resumed": "Grabación reanudada", - "aria/Recording started": "Grabación iniciada", - "aria/Remind Me Message": "Recordarme", - "aria/Remove attachment": "Eliminar adjunto", - "aria/Remove location attachment": "Eliminar adjunto de ubicación", - "aria/Remove option: {{ option }}": "Eliminar opción: {{ option }}", - "aria/Remove Reminder": "Quitar recordatorio", - "aria/Remove Save For Later": "Quitar guardar para después", - "aria/Removed option {{ option }}": "Opción {{ option }} eliminada", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "Reordenar «{{ option }}», posición {{ position }} de {{ total }}", - "aria/Reorder option {{ position }}": "Reordenar opción {{ position }}", - "aria/Resend Message": "Reenviar mensaje", - "aria/Resume recording": "Reanudar grabación", - "aria/Retry upload": "Reintentar carga", - "aria/Review bounced message": "Revisar mensaje rebotado", - "aria/Search cleared": "Búsqueda borrada", - "aria/Search results": "Resultados de búsqueda", - "aria/Search results header filter button": "Botón de filtro del encabezado de resultados de búsqueda", - "aria/Search results header filter button for: {{ source }}": "Botón de filtro del encabezado de resultados de búsqueda para: {{ source }}", - "aria/Seek audio position": "Buscar posición de audio", - "aria/Select Reaction: {{ reactionName }}": "Seleccionar reacción: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "Seleccionar canal de usuario: {{ name }}", - "aria/Send": "Enviar", - "aria/Sent": "Enviado", - "aria/Shared a link": "Enlace compartido", - "aria/Shared a link with title: {{ linkTitle }}": "Enlace compartido con título: {{ linkTitle }}", - "aria/Shared location": "Ubicación compartida", - "aria/Show preview": "Mostrar vista previa", - "aria/Start recording audio": "Iniciar grabación de audio", - "aria/Stop AI Generation": "Detener generación de IA", - "aria/Submenu": "Submenú", - "aria/Suggestions": "Sugerencias", - "aria/There are no messages in this chat.": "No hay mensajes en este chat", - "aria/This option can be reordered and removed.": "Esta opción se puede reordenar y eliminar.", - "aria/Thread list": "Lista de hilos", - "aria/Thread: {{ messagePreview }}": "Hilo: {{ messagePreview }}", - "aria/Unblock User": "Desbloquear usuario", - "aria/Unmute User": "Activar sonido", - "aria/Unpin Message": "Desfijar mensaje", - "aria/User selected: {{ user }}": "Usuario seleccionado: {{ user }}", - "aria/video": "vídeo", - "aria/voice message": "mensaje de voz", - "aria/Voice message sent": "Mensaje de voz enviado", - "aria/Voice recording attached": "Grabación de voz adjuntada", - "Ask a question": "Hacer una pregunta", - "Attach": "Adjuntar", - "Attach files": "Adjuntar archivos", - "Attachment": "Archivo adjunto", - "Attachment upload blocked due to {{reason}}": "Carga de adjunto bloqueada debido a {{reason}}", - "Attachment upload failed due to {{reason}}": "Carga de adjunto fallida debido a {{reason}}", - "Back": "Atrás", - "ban-command-args": "[@usuario] [texto]", - "ban-command-description": "Prohibir a un usuario", - "Block user": "Bloquear usuario", - "Block User": "Bloquear usuario", - "Browse channel members": "Explorar miembros del canal", - "Browse pinned messages": "Explorar mensajes fijados", - "Cancel": "Cancelar", - "Cannot seek in the recording": "No se puede buscar en la grabación", - "Changes saved": "Cambios guardados", - "Channel archived": "Canal archivado", - "Channel members": "Miembros del canal", - "Channel Missing": "Falta canal", - "Channel muted": "Canal silenciado", - "Channel pinned": "Canal fijado", - "Channel unarchived": "Canal desarchivado", - "Channel unmuted": "Silencio del canal desactivado", - "Channel unpinned": "Canal desanclado", - "Channels": "Canales", - "Chat deleted": "Chat deleted", - "Chats": "Chats", - "Choose between 2 to 10 options": "Elige entre 2 y 10 opciones", - "Close": "Cerrar", - "Close dialog": "Cerrar diálogo", - "Close emoji picker": "Cerrar el selector de emojis", - "Command not available while editing": "Comando no disponible durante la edición", - "Command not available while replying": "Comando no disponible mientras se responde", - "Commands": "Comandos", - "Commands matching": "Coincidencia de comandos", - "Connection failure, reconnecting now...": "Fallo de conexión, reconectando ahora...", - "Contact info": "Información de contacto", - "Contact name": "Nombre del contacto", - "Copy Message": "Copiar mensaje", - "Create": "Crear", - "Create a question, add options, and configure poll settings": "Crea una pregunta, añade opciones y configura los ajustes de la encuesta", - "Create poll": "Crear encuesta", - "Current location": "Ubicación actual", - "Delete": "Borrar", - "Delete chat": "Eliminar chat", - "Delete for me": "Eliminar para mí", - "Delete message": "Eliminar mensaje", - "Delivered": "Entregado", - "Direct message": "Mensaje directo", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "¿Quieres terminar esta encuesta ahora? Nadie podrá votar más en esta encuesta.", - "Download {{ fileName }}": "Descargar {{ fileName }}", - "Download All": "Descargar todo", - "Download Attachment": "Descargar archivo adjunto", - "Download attachment {{ name }}": "Descargar adjunto {{ name }}", - "Download attachment {{ number }}": "Descargar adjunto {{ number }}", - "Drag your files here": "Arrastra tus archivos aquí", - "Drag your files here to add to your post": "Arrastra tus archivos aquí para agregarlos a tu publicación", - "Due {{ timeLeft }}": "Vence en {{ timeLeft }}", - "Due since {{ dueSince }}": "Vencido desde {{ dueSince }}", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "Editar", - "Edit chat data": "Editar datos del chat", - "Edit contact": "Editar contacto", - "Edit group": "Editar grupo", - "Edit Message": "Editar mensaje", - "Edit message request failed": "Error al editar la solicitud de mensaje", - "Edited": "Editado", - "Emoji matching": "Coincidencia de emoji", - "Empty message...": "Mensaje vacío...", - "End": "Final", - "End poll": "Terminar encuesta", - "End this poll?": "¿Terminar esta encuesta?", - "End vote": "Finalizar votación", - "Enforce unique vote is enabled": "El voto único está habilitado", - "Error": "Error", - "Error · Unsent": "Error · No enviado", - "Error adding flag": "Error al agregar la bandera", - "Error adding members": "Error adding members", - "Error blocking user": "Error al bloquear al usuario", - "Error connecting to chat, refresh the page to try again.": "Error al conectarse al chat, actualice la página para volver a intentarlo.", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "Error al eliminar el mensaje", - "Error fetching reactions": "Error al cargar las reacciones", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "Error al marcar el mensaje como no leído. No se pueden marcar mensajes no leídos más antiguos que los últimos 100 mensajes del canal.", - "Error muting a user ...": "Error al silenciar el usuario...", - "Error muting channel": "Error al silenciar el canal", - "Error muting user": "Error al silenciar al usuario", - "Error opening direct message": "Error al abrir el mensaje directo", - "Error pinning message": "Error al fijar el mensaje", - "Error removing members": "Error al eliminar a los miembros", - "Error removing message pin": "Error al quitar el pin del mensaje", - "Error removing user": "Error al eliminar al usuario", - "Error reproducing the recording": "Error al reproducir la grabación", - "Error starting recording": "Error al iniciar la grabación", - "Error unblocking user": "Error al desbloquear usuario", - "Error unmuting a user ...": "Error al desactivar el silencio del usuario...", - "Error unmuting channel": "Error al desactivar el silencio del canal", - "Error unmuting user": "Error al desactivar el silencio del usuario", - "Error uploading attachment": "Error al subir el archivo adjunto", - "Error uploading file": "Error al cargar el archivo", - "Error uploading image": "Error al subir la imagen", - "Error: {{ errorMessage }}": "Error: {{ errorMessage }}", - "Exit command {{ command }}": "Salir del comando {{ command }}", - "Failed to block user": "No se pudo bloquear al usuario", - "Failed to create the poll": "Error al crear la encuesta", - "Failed to create the poll due to {{reason}}": "No se pudo crear la encuesta debido a {{reason}}", - "Failed to delete the message": "No se pudo eliminar el mensaje", - "Failed to end the poll": "No se pudo terminar la encuesta", - "Failed to end the poll due to {{reason}}": "No se pudo terminar la encuesta debido a {{reason}}", - "Failed to jump to the first unread message": "Error al saltar al primer mensaje no leído", - "Failed to leave channel": "No se pudo salir del canal", - "Failed to load channels": "No se pudieron cargar los canales", - "Failed to load more channels": "No se pudieron cargar más canales", - "Failed to mark channel as read": "Error al marcar el canal como leído", - "Failed to play the recording": "No se pudo reproducir la grabación", - "Failed to retrieve location": "No se pudo obtener la ubicación", - "Failed to save changes": "No se pudieron guardar los cambios", - "Failed to share location": "No se pudo compartir la ubicación", - "Failed to update channel archive status": "No se pudo actualizar el estado de archivo del canal", - "Failed to update channel mute status": "No se pudo actualizar el estado de silencio del canal", - "Failed to update channel pinned status": "No se pudo actualizar el estado de fijación del canal", - "File": "Archivo", - "File is required for upload attachment": "Se requiere un archivo para subir el adjunto", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "El archivo es demasiado grande: {{ size }}, el tamaño máximo de carga es de {{ limit }}", - "File too large": "Archivo demasiado grande", - "fileCount_one": "1 archivo", - "fileCount_many": "{{ count }} archivos", - "fileCount_other": "{{ count }} archivos", - "Files": "Archivos", - "Flag": "Marcar", - "Generating...": "Generando...", - "giphy-command-args": "[texto]", - "giphy-command-description": "Publicar un gif aleatorio en el canal", - "Go back": "Volver", - "Group info": "Información del grupo", - "Group name": "Nombre del grupo", - "Hide who voted": "Ocultar quién votó", - "Image": "Imagen", - "imageCount_one": "Imagen", - "imageCount_many": "{{ count }} imágenes", - "imageCount_other": "{{ count }} imágenes", - "Instant commands": "Comandos instantáneos", - "language/af": "Afrikáans", - "language/am": "Amárico", - "language/ar": "Árabe", - "language/az": "Azerbaiyano", - "language/bg": "Búlgaro", - "language/bn": "Bengalí", - "language/bs": "Bosnio", - "language/cs": "Checo", - "language/da": "Danés", - "language/de": "Alemán", - "language/el": "Griego", - "language/en": "Inglés", - "language/es": "Español", - "language/es-MX": "Español (México)", - "language/et": "Estonio", - "language/fa": "Persa", - "language/fa-AF": "Dari", - "language/fi": "Finlandés", - "language/fr": "Francés", - "language/fr-CA": "Francés (Canadá)", - "language/ha": "Hausa", - "language/he": "Hebreo", - "language/hi": "Hindi", - "language/hr": "Croata", - "language/ht": "Criollo haitiano", - "language/hu": "Húngaro", - "language/id": "Indonesio", - "language/it": "Italiano", - "language/ja": "Japonés", - "language/ka": "Georgiano", - "language/ko": "Coreano", - "language/lt": "Lituano", - "language/lv": "Letón", - "language/ms": "Malayo", - "language/nl": "Neerlandés", - "language/no": "Noruego", - "language/pl": "Polaco", - "language/ps": "Pastún", - "language/pt": "Portugués", - "language/ro": "Rumano", - "language/ru": "Ruso", - "language/sk": "Eslovaco", - "language/sl": "Esloveno", - "language/so": "Somalí", - "language/sq": "Albanés", - "language/sr": "Serbio", - "language/sv": "Sueco", - "language/sw": "Suajili", - "language/ta": "Tamil", - "language/th": "Tailandés", - "language/tl": "Tagalo", - "language/tr": "Turco", - "language/uk": "Ucraniano", - "language/ur": "Urdu", - "language/vi": "Vietnamita", - "language/zh": "Chino (simplificado)", - "language/zh-TW": "Chino (tradicional)", - "Last seen {{ timestamp }}": "Visto por última vez {{ timestamp }}", - "Leave Channel": "Abandonar canal", - "Leave chat": "Abandonar canal", - "Left channel": "Canal abandonado", - "Let others add options": "Permitir que otros añadan opciones", - "Limit votes per person": "Limitar votos por persona", - "Link": "Enlace", - "linkCount_one": "Enlace", - "linkCount_many": "{{ count }} enlaces", - "linkCount_other": "{{ count }} enlaces", - "live": "En vivo", - "Live for {{duration}}": "En vivo durante {{duration}}", - "Live location": "Ubicación en vivo", - "Live until {{ timestamp }}": "En vivo hasta {{ timestamp }}", - "Load more": "Cargar más", - "Local upload attachment missing local id": "El adjunto de subida local no tiene id local", - "Location": "Ubicación", - "Location sharing ended": "Compartir ubicación terminado", - "Location: {{ coordinates }}": "Ubicación: {{ coordinates }}", - "Manage channel": "Gestionar canal", - "Manage members": "Gestionar miembros", - "Mark as unread": "Marcar como no leído", - "Maximum number of votes (from 2 to 10)": "Número máximo de votos (de 2 a 10)", - "Maximum votes per person": "Máximo de votos por persona", - "Member detail": "Detalle del miembro", - "mention/Channel": "Canal", - "mention/Channel Description": "Notificar a todos en este canal", - "mention/Here": "Aqui", - "mention/Here Description": "Notificar a todos los miembros en línea de este canal", - "Menu": "Menú", - "Message deleted": "Mensaje eliminado", - "Message Failed · Click to try again": "Mensaje fallido · Haga clic para volver a intentarlo", - "Message Failed · Unauthorized": "Mensaje fallido · No autorizado", - "Message failed to send": "No se pudo enviar el mensaje", - "Message has been successfully flagged": "El mensaje se marcó correctamente", - "Message marked as unread": "Mensaje marcado como no leído", - "Message pinned": "Mensaje fijado", - "Message unpinned": "Mensaje desanclado", - "Message was blocked by moderation policies": "El mensaje fue bloqueado por las políticas de moderación", - "Messages have been marked unread.": "Los mensajes han sido marcados como no leídos.", - "Missing permissions to upload the attachment": "Faltan permisos para subir el archivo adjunto", - "Moderator": "Moderador", - "Multiple votes": "Votos múltiples", - "Mute": "Silenciar", - "Mute chat": "Silenciar chat", - "Mute user": "Silenciar usuario", - "mute-command-args": "[@usuario]", - "mute-command-description": "Silenciar a un usuario", - "network error": "error de red", - "New": "Nuevo", - "New message from {{user}}": "Nuevo mensaje de {{user}}", - "New Messages!": "¡Nuevos mensajes!", - "Next": "Siguiente", - "Next image": "Siguiente imagen", - "No chats here yet…": "Aún no hay mensajes aquí...", - "No conversations yet": "Aún no hay conversaciones", - "No files": "No hay archivos", - "No items exist": "No existen elementos", - "No member found": "No se encontró ningún miembro", - "No messages found": "No se encontraron mensajes", - "No photos or videos": "No hay fotos ni videos", - "No pinned messages": "No hay mensajes fijados", - "No results found": "No se han encontrado resultados", - "No user found": "No se encontró ningún usuario", - "Nobody will be able to vote in this poll anymore.": "Nadie podrá votar en esta encuesta.", - "Nothing yet...": "Nada aún...", - "Notify all {{ role }} members": "Notificar a todos los miembros con rol {{ role }}", - "Offline": "Desconectado", - "Ok": "Aceptar", - "Online": "En línea", - "Only numbers are allowed": "Solo se permiten números", - "Only visible to you": "Solo visible para ti", - "Open emoji picker": "Abrir el selector de emojis", - "Open gallery at image {{ index }}": "Abrir galería en la imagen {{ index }}", - "Open image in gallery": "Abrir imagen en la galería", - "Open location in a map": "Abrir ubicación en un mapa", - "Open members actions": "Open members actions", - "Open menu": "Abrir menú", - "Option already exists": "La opción ya existe", - "Option is empty": "La opción está vacía", - "Options": "Opciones", - "Original": "Original", - "Owner": "Propietario", - "People matching": "Personas que coinciden", - "Photo": "Foto", - "Photos & videos": "Fotos y videos", - "Pin": "Fijar", - "Pin a message to see it here": "Fija un mensaje para verlo aquí", - "Pinned by {{ name }}": "Fijado por {{ name }}", - "Pinned by You": "Anclado por ti", - "Pinned message": "Mensaje fijado", - "Pinned messages": "Mensajes fijados", - "placeholder/PollComment": "Tu comentario", - "placeholder/PollOptionSuggestion": "Introduce una nueva opción", - "Play video": "Reproducir video", - "Playback speed {{ rate }}x": "Velocidad de reproducción {{ rate }}x", - "Poll": "Encuesta", - "Poll comments": "Comentarios de la encuesta", - "Poll ended": "Encuesta finalizada", - "Poll options": "Opciones de la encuesta", - "Poll results": "Resultados de la encuesta", - "Poll sent": "Encuesta enviada", - "Previous": "Anterior", - "Previous image": "Imagen anterior", - "Question": "Pregunta", - "Question {{ optionOrderNumber}}": "Pregunta {{ optionOrderNumber}}", - "Question is required": "La pregunta es obligatoria", - "Quote Reply": "Responder con cita", - "Reached the vote limit. Remove an existing vote first.": "Se ha alcanzado el límite de votos. Elimina un voto existente primero.", - "Recording format is not supported and cannot be reproduced": "El formato de grabación no es compatible y no se puede reproducir", - "Remind me": "Recordarme", - "Remind Me": "Recordarme", - "Reminder set": "Recordatorio establecido", - "Remove": "Eliminar", - "Remove {{ count }} members_one": "Eliminar {{ count }} miembro", - "Remove {{ count }} members_many": "Eliminar {{ count }} miembros", - "Remove {{ count }} members_other": "Eliminar {{ count }} miembros", - "Remove {{ member }} from this channel?": "¿Eliminar a {{ member }} de este canal?", - "Remove channel members": "Eliminar miembros del canal", - "Remove reminder": "Eliminar recordatorio", - "Remove save for later": "Quitar guardar para después", - "Remove user": "Eliminar usuario", - "Removed {{ count }} members_one": "Se eliminó {{ count }} miembro", - "Removed {{ count }} members_many": "Se eliminaron {{ count }} miembros", - "Removed {{ count }} members_other": "Se eliminaron {{ count }} miembros", - "Replied to a thread": "Respondió en un hilo", - "Reply": "Responder", - "Reply to {{ authorName }}": "Responder a {{ authorName }}", - "Reply to a message to start a thread": "Responde a un mensaje para iniciar un hilo", - "Reply to Message": "Responder al mensaje", - "replyCount_one": "1 respuesta", - "replyCount_many": "{{ count }} respuestas", - "replyCount_other": "{{ count }} respuestas", - "Resend": "Reenviar", - "Retry upload": "Reintentar la carga", - "Review all options available in this poll": "Revisa todas las opciones disponibles en esta encuesta", - "Review comments submitted with poll answers": "Revisa los comentarios enviados con las respuestas de la encuesta", - "Review poll results and open an option to see detailed votes": "Revisa los resultados de la encuesta y abre una opción para ver votos detallados", - "Review this message and choose whether to delete it, edit it, or send it anyway": "Revisa este mensaje y elige si eliminarlo, editarlo o enviarlo de todos modos", - "Review who voted for this option": "Revisa quién votó por esta opción", - "Save": "Guardar", - "Save for later": "Guardar para más tarde", - "Saved for later": "Guardado para más tarde", - "Search": "Buscar", - "Search GIFs": "Buscar GIFs", - "search-results-header-filter-source-button-label--channels": "canales", - "search-results-header-filter-source-button-label--messages": "mensajes", - "search-results-header-filter-source-button-label--users": "usuarios", - "Searching for {{ searchSourceType }}...": "Buscando {{ searchSourceType }}...", - "Searching...": "Buscando...", - "searchResultsCount_one": "1 resultado", - "searchResultsCount_many": "{{ count }} resultados", - "searchResultsCount_other": "{{ count }} resultados", - "See all options ({{count}})_one": "Ver todas las opciones ({{count}})", - "See all options ({{count}})_many": "Ver todas las opciones ({{count}})", - "See all options ({{count}})_other": "Ver todas las opciones ({{count}})", - "Select a thread to continue the conversation": "Selecciona un hilo para continuar la conversación", - "Select more than one option": "Seleccionar más de una opción", - "Select one": "Seleccionar uno", - "Select one or more": "Seleccionar uno o más", - "Select up to {{count}}_one": "Selecciona hasta {{count}}", - "Select up to {{count}}_many": "Selecciona hasta {{count}}", - "Select up to {{count}}_other": "Selecciona hasta {{count}}", - "Select your current location and optionally enable live location sharing": "Selecciona tu ubicación actual y, opcionalmente, habilita el uso compartido de ubicación en vivo", - "Send": "Enviar", - "Send a message": "Envía un mensaje", - "Send a message to start the conversation": "Envía un mensaje para iniciar la conversación", - "Send Anyway": "Enviar de todos modos", - "Send direct message": "Enviar mensaje directo", - "Send message request failed": "Error al enviar la solicitud de mensaje", - "Send poll": "Enviar encuesta", - "Sending...": "Enviando...", - "Sent": "Enviado", - "Share": "Compartir", - "Share a file to see it here": "Comparte un archivo para verlo aquí", - "Share a photo or video to see it here": "Comparte una foto o un video para verlo aquí", - "Share live location for": "Compartir ubicación en vivo durante", - "Share Location": "Compartir ubicación", - "Shared live location": "Ubicación en vivo compartida", - "Shared location": "Ubicación compartida", - "Show all": "Mostrar todo", - "Shuffle": "Mezclar", - "size limit": "límite de tamaño", - "Slow Mode ON": "Modo lento activado", - "Slow mode, wait {{ seconds }}s...": "Modo lento, espera {{ seconds }} s...", - "Some of the files will not be accepted": "Algunos archivos no serán aceptados", - "Start typing to search": "Empieza a escribir para buscar", - "Stop sharing": "Dejar de compartir", - "Submit": "Enviar", - "Suggest a new option to add to this poll": "Sugiere una nueva opción para añadir a esta encuesta", - "Suggest an option": "Sugerir una opción", - "Tap to remove": "Toca para quitar", - "Tap to remove: {{ reactionName }}": "Toca para quitar: {{ reactionName }}", - "Thinking...": "Pensando...", - "this content could not be displayed": "Este contenido no se pudo mostrar", - "This field cannot be empty or contain only spaces": "Este campo no puede estar vacío o contener solo espacios", - "This message did not meet our content guidelines": "Este mensaje no cumple con nuestras directrices de contenido", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "Este usuario podrá enviarte mensajes de nuevo.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "Hilo", - "Thread has not been found": "No se ha encontrado el hilo", - "Thread reply": "Respuesta en hilo", - "Thread Reply": "Respuesta en hilo", - "ThreadListUnseenThreadsBanner/loading": "Cargando...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} hilo no leído", - "ThreadListUnseenThreadsBanner/unreadThreads_many": "{{ count }} hilos no leídos", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} hilos no leídos", - "Threads": "Hilos", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Ayer]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoy]\", \"nextDay\": \"[Mañana]\", \"lastDay\": \"[Ayer]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Último] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "hace {{ count }} d", - "timestamp/relativeToday": "Hoy", - "timestamp/relativeWeeksAgo": "hace {{ count }} sem", - "timestamp/relativeYesterday": "Ayer", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoy] [a las] HH:mm\", \"nextDay\": \"[Mañana] [a las] HH:mm\", \"lastDay\": \"[Ayer] [a las] HH:mm\", \"nextWeek\": \"dddd [a las] HH:mm\", \"lastWeek\": \"[Último] dddd [a las] HH:mm\", \"sameElse\": \"ddd, D MMM [a las] HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "Para comenzar a grabar, permita el acceso a la cámara en su navegador", - "To start recording, allow the microphone access in your browser": "Para comenzar a grabar, permita el acceso al micrófono en su navegador", - "totalVoteCount_one": "1 voto en total", - "totalVoteCount_many": "{{ count }} votos en total", - "totalVoteCount_other": "{{ count }} votos en total", - "Translated": "Traducido", - "Translated from {{ language }}": "Traducido de {{ language }}", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "Escribe un número del 2 al 10", - "Type your message": "Escribe tu mensaje", - "Unarchive": "Desarchivar", - "unban-command-args": "[@usuario]", - "unban-command-description": "Quitar la prohibición a un usuario", - "Unblock": "Desbloquear", - "Unblock user": "Desbloquear usuario", - "Unblock User": "Desbloquear usuario", - "unknown error": "error desconocido", - "Unmute": "Activar sonido", - "Unmute chat": "Desactivar silencio del chat", - "Unmute user": "Desactivar silencio del usuario", - "unmute-command-args": "[@usuario]", - "unmute-command-description": "Desactivar el silencio de un usuario", - "Unpin": "Desfijar", - "Unread messages": "Mensajes no leídos", - "Unsupported attachment": "Adjunto no compatible", - "unsupported file type": "tipo de archivo no compatible", - "Update": "Actualizar", - "Update the comment attached to your poll answer": "Actualiza el comentario adjunto a tu respuesta de la encuesta", - "Update your comment": "Actualizar tu comentario", - "Upload blocked": "Carga bloqueada", - "Upload error": "Error de carga", - "Upload failed": "Carga fallida", - "Upload Picture": "Subir imagen", - "Upload type: \"{{ type }}\" is not allowed": "Tipo de carga: \"{{ type }}\" no está permitido", - "User blocked": "Usuario bloqueado", - "User muted": "Usuario silenciado", - "User removed": "Usuario eliminado", - "User unblocked": "Usuario desbloqueado", - "User unmuted": "Usuario con silencio desactivado", - "User uploaded content": "Contenido subido por el usuario", - "Video": "Vídeo", - "videoCount_one": "Video", - "videoCount_many": "{{ count }} videos", - "videoCount_other": "{{ count }} videos", - "View": "Ver", - "View {{count}} comments_one": "Ver {{count}} comentario", - "View {{count}} comments_many": "Ver {{count}} comentarios", - "View {{count}} comments_other": "Ver {{count}} comentarios", - "View all": "Ver todo", - "View member details for {{ member }}": "Ver detalles del miembro {{ member }}", - "View original": "Ver original", - "View results": "Ver resultados", - "View translation": "Ver traducción", - "Voice message": "Mensaje de voz", - "Voice message {{ duration }}": "Mensaje de voz {{ duration }}", - "Voice message deleted": "Mensaje de voz eliminado", - "voiceMessageCount_one": "Mensaje de voz", - "voiceMessageCount_many": "{{ count }} mensajes de voz", - "voiceMessageCount_other": "{{ count }} mensajes de voz", - "Vote ended": "Votación finalizada", - "Votes": "Votos", - "Wait until all attachments have uploaded": "Espere hasta que se hayan cargado todos los archivos adjuntos", - "Waiting for network…": "Esperando red…", - "You": "Tú", - "You have no channels currently": "Actualmente no tienes canales", - "You've reached the maximum number of files": "Has alcanzado el número máximo de archivos" -} diff --git a/src/i18n/fr.json b/src/i18n/fr.json deleted file mode 100644 index 26fb592fa3..0000000000 --- a/src/i18n/fr.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} et {{ moreCount }} autres", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} et {{ lastUser }}", - "{{ count }} files_one": "{{ count }} fichier", - "{{ count }} files_many": "{{ count }} fichiers", - "{{ count }} files_other": "{{ count }} fichiers", - "{{ count }} members_one": "{{ count }} membre", - "{{ count }} members_many": "{{ count }} membres", - "{{ count }} members_other": "{{ count }} membres", - "{{ count }} members added_one": "{{ count }} membre ajouté", - "{{ count }} members added_many": "{{ count }} membres ajoutés", - "{{ count }} members added_other": "{{ count }} membres ajoutés", - "{{ count }} people are typing_one": "{{ count }} personne écrit", - "{{ count }} people are typing_many": "{{ count }} personnes écrivent", - "{{ count }} people are typing_other": "{{ count }} personnes écrivent", - "{{ count }} photos_one": "{{ count }} photo", - "{{ count }} photos_many": "{{ count }} photos", - "{{ count }} photos_other": "{{ count }} photos", - "{{ count }} reactions_one": "{{ count }} réaction", - "{{ count }} reactions_many": "{{ count }} réactions", - "{{ count }} reactions_other": "{{ count }} réactions", - "{{ count }} videos_one": "{{ count }} vidéo", - "{{ count }} videos_many": "{{ count }} vidéos", - "{{ count }} videos_other": "{{ count }} vidéos", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} et {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }} supplémentaires", - "{{ member }} will be able to message you again.": "{{ member }} pourra à nouveau vous envoyer des messages.", - "{{ member }} won't be able to message you anymore.": "{{ member }} ne pourra plus vous envoyer de messages.", - "{{ memberCount }} members": "{{ memberCount }} membres", - "{{ typing }} are typing": "{{ typing }} écrivent", - "{{ typing }} is typing": "{{ typing }} écrit", - "{{ user }} has been muted": "{{ user }} a été mis en sourdine", - "{{ user }} has been unmuted": "{{ user }} n'est plus en sourdine", - "{{ user }} is typing...": "{{ user }} est en train d'écrire...", - "{{ users }} and {{ user }} are typing...": "{{ users }} et {{ user }} sont en train d'écrire...", - "{{ users }} and more are typing...": "{{ users }} et plus sont en train d'écrire...", - "{{ watcherCount }} online": "{{ watcherCount }} en ligne", - "{{count}} new messages_one": "{{count}} nouveau message", - "{{count}} new messages_many": "{{count}} nouveaux messages", - "{{count}} new messages_other": "{{count}} nouveaux messages", - "{{count}} unread_one": "{{count}} non lu", - "{{count}} unread_many": "{{count}} non lus", - "{{count}} unread_other": "{{count}} non lus", - "{{count}} votes_one": "{{count}} vote", - "{{count}} votes_many": "{{count}} votes", - "{{count}} votes_other": "{{count}} votes", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+{{count}} option de plus", - "+{{count}} more options_many": "+{{count}} options de plus", - "+{{count}} more options_other": "+{{count}} options de plus", - "🏙 Attachment...": "🏙 Pièce jointe...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} a créé : {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} a voté : {{pollOptionText}}", - "📍Shared location": "📍Emplacement partagé", - "Actions": "Actions", - "Add": "Ajouter", - "Add {{ count }} members_one": "Ajouter {{ count }} membre", - "Add {{ count }} members_many": "Ajouter {{ count }} membres", - "Add {{ count }} members_other": "Ajouter {{ count }} membres", - "Add a comment": "Ajouter un commentaire", - "Add a comment to your poll answer": "Ajoutez un commentaire à votre réponse au sondage", - "Add an option": "Ajouter une option", - "Add channel members": "Ajouter des membres au canal", - "Add members": "Ajouter des membres", - "Add reaction": "Ajouter une réaction", - "Admin": "Admin", - "All results loaded": "Tous les résultats sont chargés", - "Allow access to camera": "Autoriser l'accès à la caméra", - "Allow access to microphone": "Autoriser l'accès au microphone", - "Allow comments": "Autoriser les commentaires", - "Allow option suggestion": "Autoriser la suggestion d'options", - "Allow others to add comments": "Permettre à d'autres d'ajouter des commentaires", - "Already a member": "Déjà membre", - "Also send as a direct message": "Également envoyer en message direct", - "Also send in channel": "Également envoyer dans le canal", - "Also sent in channel": "Également envoyé dans le canal", - "An error has occurred during recording": "Une erreur s'est produite pendant l'enregistrement", - "An error has occurred during the recording processing": "Une erreur s'est produite pendant le traitement de l'enregistrement", - "Anonymous": "Anonyme", - "Anonymous poll": "Sondage anonyme", - "Archive": "Archiver", - "Are you sure you want to delete this message?": "Êtes-vous sûr de vouloir supprimer ce message ?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_many": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} pièce jointe", - "aria/{{ count }} attachment_many": "{{ count }} pièces jointes", - "aria/{{ count }} attachment_other": "{{ count }} pièces jointes", - "aria/{{ count }} search results_one": "{{ count }} résultat de recherche", - "aria/{{ count }} search results_many": "{{ count }} résultats de recherche", - "aria/{{ count }} search results_other": "{{ count }} résultats de recherche", - "aria/{{ count }} suggestions_one": "{{ count }} suggestion", - "aria/{{ count }} suggestions_many": "{{ count }} suggestions", - "aria/{{ count }} suggestions_other": "{{ count }} suggestions", - "aria/{{ count }} unread message_one": "{{ count }} message non lu", - "aria/{{ count }} unread message_many": "{{ count }} messages non lus", - "aria/{{ count }} unread message_other": "{{ count }} messages non lus", - "aria/{{ setting }} disabled": "{{ setting }} désactivé", - "aria/{{ setting }} enabled": "{{ setting }} activé", - "aria/Active": "Actif", - "aria/Animated GIF": "GIF animé", - "aria/Animated GIF: {{ title }}": "GIF animé : {{ title }}", - "aria/Attachment": "Pièce jointe", - "aria/Attachment {{ attachmentType }}": "Pièce jointe {{ attachmentType }}", - "aria/Attachment Actions": "Actions de la pièce jointe", - "aria/audio": "audio", - "aria/Audio position {{ elapsed }} of {{ duration }}": "Position audio {{ elapsed }} sur {{ duration }}", - "aria/Audio position {{ progress }} percent": "Position audio {{ progress }} pour cent", - "aria/Back to attachments": "Retour aux pièces jointes", - "aria/Back to parent menu button": "Retour au menu parent bouton", - "aria/Block User": "Bloquer l'utilisateur", - "aria/Bookmark Message": "Enregistrer le message", - "aria/Cancel recording": "Annuler l'enregistrement", - "aria/Cancel Reply": "Annuler la réponse", - "aria/Channel Actions": "Actions du canal", - "aria/Channel details": "Détails du canal", - "aria/Channel list": "Liste des canaux", - "aria/Chat view controls": "Commandes de la vue du chat", - "aria/Chat: {{ channelName }}": "Chat : {{ channelName }}", - "aria/Clear search": "Effacer la recherche", - "aria/Close callout dialog": "Fermer la boîte de dialogue d'information", - "aria/Close thread": "Fermer le fil", - "aria/Collapse sidebar": "Réduire la barre latérale", - "aria/Command activated: {{ command }}": "Commande activée : {{ command }}", - "aria/Command Suggestions": "Suggestions de commandes", - "aria/Complete recording": "Terminer l'enregistrement", - "aria/Copy Message Text": "Copier le texte du message", - "aria/Decrease value": "Diminuer la valeur", - "aria/Delete Message": "Supprimer le message", - "aria/Delivered": "Remis", - "aria/Delivery status: {{ deliveryStatus }}": "Statut de remise : {{ deliveryStatus }}", - "aria/Dismiss notification": "Ignorer la notification", - "aria/Download attachment": "Télécharger la pièce jointe", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "« {{ option }} » déposé à la position {{ position }}.", - "aria/Edit Message": "Éditer un message", - "aria/Emoji picker": "Sélecteur d'émojis", - "aria/Emoji Suggestions": "Suggestions d'émojis", - "aria/Exit search": "Quitter la recherche", - "aria/Expand sidebar": "Développer la barre latérale", - "aria/file": "fichier", - "aria/File upload": "Téléchargement de fichier", - "aria/Flag Message": "Signaler le message", - "aria/GIF": "GIF", - "aria/Giphy actions": "Actions Giphy", - "aria/Giphy canceled": "Giphy annulé", - "aria/Giphy image changed": "Image Giphy modifiée", - "aria/Giphy image changed: {{ title }}": "Image Giphy modifiée : {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Aperçu Giphy, visible uniquement par vous. Utilisez les actions Envoyer, Mélanger ou Annuler.", - "aria/Giphy sent": "Giphy envoyé", - "aria/Go back": "Retour", - "aria/image": "image", - "aria/Image failed to load": "Échec du chargement de l'image", - "aria/Increase value": "Augmenter la valeur", - "aria/Jump to latest message": "Aller au dernier message", - "aria/Jump to quoted message": "Aller au message cité", - "aria/Last activity: {{ time }}": "Dernière activité : {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "Dernier message de {{ sender }} : {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "Dernier message : {{ messagePreview }}", - "aria/Mark Message Unread": "Marquer comme non lu", - "aria/Mark messages as read": "Marquer les messages comme lus", - "aria/Mention Suggestions": "Suggestions de mentions", - "aria/Message Actions": "Actions du message", - "aria/Message from {{ user }},": "Message de {{ user }},", - "aria/Message input": "Saisie du message", - "aria/Message with attachments": "Message avec des pièces jointes", - "aria/Message,": "Message,", - "aria/Mute User": "Mettre en sourdine", - "aria/Next page": "Page suivante", - "aria/No search results found": "Aucun résultat de recherche", - "aria/Notifications": "Notifications", - "aria/Open Attachment Selector": "Ouvrir le sélecteur de pièces jointes", - "aria/Open Channel Actions Menu": "Ouvrir le menu des actions du canal", - "aria/Open channel details": "Ouvrir les détails du canal", - "aria/Open channels view": "Ouvrir la vue des canaux", - "aria/Open image shared by {{ name }}": "Ouvrir l'image partagée par {{ name }}", - "aria/Open Message Actions Menu": "Ouvrir le menu des actions du message", - "aria/Open Reaction Selector": "Ouvrir le sélecteur de réactions", - "aria/Open Thread": "Ouvrir le fil", - "aria/Open threads view": "Ouvrir la vue des fils", - "aria/Open threads view with unread threads_one": "Ouvrir la vue des fils, {{ count }} fil non lu", - "aria/Open threads view with unread threads_many": "Ouvrir la vue des fils, {{ count }} fils non lus", - "aria/Open threads view with unread threads_other": "Ouvrir la vue des fils, {{ count }} fils non lus", - "aria/Open video shared by {{ name }}": "Ouvrir la vidéo partagée par {{ name }}", - "aria/Opened channel: {{ name }}": "Canal ouvert : {{ name }}", - "aria/Opened thread in {{ name }}": "Fil ouvert dans {{ name }}", - "aria/Option {{ position }}": "Option {{ position }}", - "aria/Options can now be reordered and removed.": "Les options peuvent désormais être réorganisées et supprimées.", - "aria/Pause": "Pause", - "aria/Pause recording": "Mettre en pause l'enregistrement", - "aria/Percent complete": "{{percent}} pour cent terminé", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "« {{ option }} » saisi. Utilisez les flèches pour réorganiser. Appuyez sur Espace ou Tab pour déposer.", - "aria/Pin Message": "Épingler le message", - "aria/Play": "Lecture", - "aria/Poll dialog opened": "Boîte de dialogue du sondage ouverte", - "aria/Poll sent": "Sondage envoyé", - "aria/Poll: {{ pollName }}": "Sondage : {{ pollName }}", - "aria/Press Enter to start typing": "Appuyez sur Entrée pour commencer à saisir", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "Appuyez sur la barre d'espace pour sélectionner cette option, utilisez les flèches Haut et Bas pour la déplacer, puis appuyez à nouveau sur la barre d'espace pour la désélectionner.", - "aria/Previous page": "Page précédente", - "aria/Quote Message": "Citer le message", - "aria/Reaction list": "Liste des réactions", - "aria/Read": "Lu", - "aria/Recording paused": "Enregistrement en pause", - "aria/Recording resumed": "Enregistrement repris", - "aria/Recording started": "Enregistrement démarré", - "aria/Remind Me Message": "Me rappeler", - "aria/Remove attachment": "Supprimer la pièce jointe", - "aria/Remove location attachment": "Supprimer la pièce jointe d'emplacement", - "aria/Remove option: {{ option }}": "Supprimer l'option : {{ option }}", - "aria/Remove Reminder": "Supprimer le rappel", - "aria/Remove Save For Later": "Supprimer « Enregistrer pour plus tard »", - "aria/Removed option {{ option }}": "Option {{ option }} supprimée", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "Réorganiser « {{ option }} », position {{ position }} sur {{ total }}", - "aria/Reorder option {{ position }}": "Réorganiser l'option {{ position }}", - "aria/Resend Message": "Renvoyer le message", - "aria/Resume recording": "Reprendre l'enregistrement", - "aria/Retry upload": "Réessayer le téléchargement", - "aria/Review bounced message": "Examiner le message rejeté", - "aria/Search cleared": "Recherche effacée", - "aria/Search results": "Résultats de recherche", - "aria/Search results header filter button": "Bouton de filtre d'en-tête des résultats de recherche", - "aria/Search results header filter button for: {{ source }}": "Bouton de filtre d'en-tête des résultats de recherche pour : {{ source }}", - "aria/Seek audio position": "Rechercher la position audio", - "aria/Select Reaction: {{ reactionName }}": "Sélectionner la réaction : {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "Sélectionner le canal utilisateur : {{ name }}", - "aria/Send": "Envoyer", - "aria/Sent": "Envoyé", - "aria/Shared a link": "Lien partagé", - "aria/Shared a link with title: {{ linkTitle }}": "Lien partagé avec le titre : {{ linkTitle }}", - "aria/Shared location": "Position partagée", - "aria/Show preview": "Afficher l'aperçu", - "aria/Start recording audio": "Commencer l'enregistrement audio", - "aria/Stop AI Generation": "Arrêter la génération d'IA", - "aria/Submenu": "Sous-menu", - "aria/Suggestions": "Suggestions", - "aria/There are no messages in this chat.": "Aucun message dans cette discussion", - "aria/This option can be reordered and removed.": "Cette option peut être réorganisée et supprimée.", - "aria/Thread list": "Liste des fils", - "aria/Thread: {{ messagePreview }}": "Fil : {{ messagePreview }}", - "aria/Unblock User": "Débloquer l'utilisateur", - "aria/Unmute User": "Désactiver muet", - "aria/Unpin Message": "Détacher le message", - "aria/User selected: {{ user }}": "Utilisateur sélectionné : {{ user }}", - "aria/video": "vidéo", - "aria/voice message": "message vocal", - "aria/Voice message sent": "Message vocal envoyé", - "aria/Voice recording attached": "Enregistrement vocal joint", - "Ask a question": "Poser une question", - "Attach": "Joindre", - "Attach files": "Joindre des fichiers", - "Attachment": "Pièce jointe", - "Attachment upload blocked due to {{reason}}": "Téléchargement de pièce jointe bloqué en raison de {{reason}}", - "Attachment upload failed due to {{reason}}": "Échec du téléchargement de la pièce jointe en raison de {{reason}}", - "Back": "Retour", - "ban-command-args": "[@nomdutilisateur] [texte]", - "ban-command-description": "Bannir un utilisateur", - "Block user": "Bloquer l'utilisateur", - "Block User": "Bloquer l'utilisateur", - "Browse channel members": "Parcourir les membres du canal", - "Browse pinned messages": "Parcourir les messages épinglés", - "Cancel": "Annuler", - "Cannot seek in the recording": "Impossible de rechercher dans l'enregistrement", - "Changes saved": "Modifications enregistrées", - "Channel archived": "Canal archivé", - "Channel members": "Membres du canal", - "Channel Missing": "Canal Manquant", - "Channel muted": "Canal mis en sourdine", - "Channel pinned": "Canal épinglé", - "Channel unarchived": "Canal désarchivé", - "Channel unmuted": "Sourdine du canal désactivée", - "Channel unpinned": "Canal désépinglé", - "Channels": "Canaux", - "Chat deleted": "Chat deleted", - "Chats": "Discussions", - "Choose between 2 to 10 options": "Choisir entre 2 et 10 options", - "Close": "Fermer", - "Close dialog": "Fermer la boîte de dialogue", - "Close emoji picker": "Fermer le sélecteur d'émojis", - "Command not available while editing": "Commande non disponible pendant la modification", - "Command not available while replying": "Commande non disponible pendant la réponse", - "Commands": "Commandes", - "Commands matching": "Correspondance des commandes", - "Connection failure, reconnecting now...": "Échec de la connexion, reconnexion en cours...", - "Contact info": "Informations du contact", - "Contact name": "Nom du contact", - "Copy Message": "Copier le message", - "Create": "Créer", - "Create a question, add options, and configure poll settings": "Créez une question, ajoutez des options et configurez les paramètres du sondage", - "Create poll": "Créer un sondage", - "Current location": "Emplacement actuel", - "Delete": "Supprimer", - "Delete chat": "Supprimer le chat", - "Delete for me": "Supprimer pour moi", - "Delete message": "Supprimer le message", - "Delivered": "Publié", - "Direct message": "Message direct", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "Voulez-vous terminer ce sondage maintenant ? Personne ne pourra plus voter dans ce sondage.", - "Download {{ fileName }}": "Télécharger {{ fileName }}", - "Download All": "Tout télécharger", - "Download Attachment": "Télécharger la pièce jointe", - "Download attachment {{ name }}": "Télécharger la pièce jointe {{ name }}", - "Download attachment {{ number }}": "Télécharger la pièce jointe {{ number }}", - "Drag your files here": "Glissez vos fichiers ici", - "Drag your files here to add to your post": "Glissez vos fichiers ici pour les ajouter à votre publication", - "Due {{ timeLeft }}": "Échéance dans {{ timeLeft }}", - "Due since {{ dueSince }}": "Échéance depuis {{ dueSince }}", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "Modifier", - "Edit chat data": "Modifier les données du chat", - "Edit contact": "Modifier le contact", - "Edit group": "Modifier le groupe", - "Edit Message": "Éditer un message", - "Edit message request failed": "Échec de la demande de modification du message", - "Edited": "Modifié", - "Emoji matching": "Correspondance d'émojis", - "Empty message...": "Message vide...", - "End": "Fin", - "End poll": "Terminer le sondage", - "End this poll?": "Terminer ce sondage ?", - "End vote": "Fin du vote", - "Enforce unique vote is enabled": "Le vote unique est activé", - "Error": "Erreur", - "Error · Unsent": "Erreur - Non envoyé", - "Error adding flag": "Erreur lors de l'ajout du signalement", - "Error adding members": "Error adding members", - "Error blocking user": "Erreur lors du blocage de l'utilisateur", - "Error connecting to chat, refresh the page to try again.": "Erreur de connexion au chat, rafraîchissez la page pour réessayer.", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "Erreur lors de la suppression du message", - "Error fetching reactions": "Erreur lors du chargement des réactions", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "Erreur lors de la marque du message comme non lu. Impossible de marquer des messages non lus plus anciens que les 100 derniers messages du canal.", - "Error muting a user ...": "Erreur lors de la mise en sourdine d'un utilisateur...", - "Error muting channel": "Erreur lors de la mise en sourdine du canal", - "Error muting user": "Erreur lors de la mise en sourdine de l'utilisateur", - "Error opening direct message": "Erreur lors de l'ouverture du message direct", - "Error pinning message": "Erreur lors de l'épinglage du message", - "Error removing members": "Erreur lors du retrait des membres", - "Error removing message pin": "Erreur lors du retrait de l'épinglage du message", - "Error removing user": "Erreur lors du retrait de l'utilisateur", - "Error reproducing the recording": "Erreur lors de la reproduction de l'enregistrement", - "Error starting recording": "Erreur lors du démarrage de l'enregistrement", - "Error unblocking user": "Erreur lors du déblocage de l'utilisateur", - "Error unmuting a user ...": "Erreur lors du démarrage de la sourdine d'un utilisateur ...", - "Error unmuting channel": "Erreur lors de la désactivation de la sourdine du canal", - "Error unmuting user": "Erreur lors de la désactivation de la sourdine de l'utilisateur", - "Error uploading attachment": "Erreur lors du téléchargement de la pièce jointe", - "Error uploading file": "Erreur lors du téléchargement du fichier", - "Error uploading image": "Erreur lors de l'envoi de l'image", - "Error: {{ errorMessage }}": "Erreur : {{ errorMessage }}", - "Exit command {{ command }}": "Quitter la commande {{ command }}", - "Failed to block user": "Impossible de bloquer l'utilisateur", - "Failed to create the poll": "Échec de la création du sondage", - "Failed to create the poll due to {{reason}}": "Échec de la création du sondage en raison de {{reason}}", - "Failed to delete the message": "Échec de la suppression du message", - "Failed to end the poll": "Impossible de terminer le sondage", - "Failed to end the poll due to {{reason}}": "Impossible de terminer le sondage en raison de {{reason}}", - "Failed to jump to the first unread message": "Échec du saut vers le premier message non lu", - "Failed to leave channel": "Impossible de quitter le canal", - "Failed to load channels": "Impossible de charger les canaux", - "Failed to load more channels": "Impossible de charger davantage de canaux", - "Failed to mark channel as read": "Échec du marquage du canal comme lu", - "Failed to play the recording": "Impossible de lire l'enregistrement", - "Failed to retrieve location": "Impossible de récupérer l'emplacement", - "Failed to save changes": "Échec de l'enregistrement des modifications", - "Failed to share location": "Impossible de partager l'emplacement", - "Failed to update channel archive status": "Impossible de mettre à jour l'état d'archivage du canal", - "Failed to update channel mute status": "Impossible de mettre à jour l'état de la mise en sourdine du canal", - "Failed to update channel pinned status": "Impossible de mettre à jour l'état d'épinglage du canal", - "File": "Fichier", - "File is required for upload attachment": "Un fichier est requis pour joindre une pièce", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "Le fichier est trop volumineux : {{ size }}, la taille maximale de téléchargement est de {{ limit }}", - "File too large": "Fichier trop volumineux", - "fileCount_one": "1 fichier", - "fileCount_many": "{{ count }} fichiers", - "fileCount_other": "{{ count }} fichiers", - "Files": "Fichiers", - "Flag": "Signaler", - "Generating...": "Génération...", - "giphy-command-args": "[texte]", - "giphy-command-description": "Poster un GIF aléatoire dans le canal", - "Go back": "Retour", - "Group info": "Informations du groupe", - "Group name": "Nom du groupe", - "Hide who voted": "Masquer qui a voté", - "Image": "Image", - "imageCount_one": "Photo", - "imageCount_many": "{{ count }} photos", - "imageCount_other": "{{ count }} photos", - "Instant commands": "Commandes instantanées", - "language/af": "Afrikaans", - "language/am": "Amharique", - "language/ar": "Arabe", - "language/az": "Azéri", - "language/bg": "Bulgare", - "language/bn": "Bengali", - "language/bs": "Bosnien", - "language/cs": "Tchèque", - "language/da": "Danois", - "language/de": "Allemand", - "language/el": "Grec", - "language/en": "Anglais", - "language/es": "Espagnol", - "language/es-MX": "Espagnol (Mexique)", - "language/et": "Estonien", - "language/fa": "Persan", - "language/fa-AF": "Dari", - "language/fi": "Finnois", - "language/fr": "Français", - "language/fr-CA": "Français (Canada)", - "language/ha": "Haoussa", - "language/he": "Hébreu", - "language/hi": "Hindi", - "language/hr": "Croate", - "language/ht": "Créole haïtien", - "language/hu": "Hongrois", - "language/id": "Indonésien", - "language/it": "Italien", - "language/ja": "Japonais", - "language/ka": "Géorgien", - "language/ko": "Coréen", - "language/lt": "Lituanien", - "language/lv": "Letton", - "language/ms": "Malais", - "language/nl": "Néerlandais", - "language/no": "Norvégien", - "language/pl": "Polonais", - "language/ps": "Pachto", - "language/pt": "Portugais", - "language/ro": "Roumain", - "language/ru": "Russe", - "language/sk": "Slovaque", - "language/sl": "Slovène", - "language/so": "Somali", - "language/sq": "Albanais", - "language/sr": "Serbe", - "language/sv": "Suédois", - "language/sw": "Swahili", - "language/ta": "Tamoul", - "language/th": "Thaï", - "language/tl": "Tagalog", - "language/tr": "Turc", - "language/uk": "Ukrainien", - "language/ur": "Ourdou", - "language/vi": "Vietnamien", - "language/zh": "Chinois (simplifié)", - "language/zh-TW": "Chinois (traditionnel)", - "Last seen {{ timestamp }}": "Vu pour la dernière fois {{ timestamp }}", - "Leave Channel": "Quitter le canal", - "Leave chat": "Quitter le canal", - "Left channel": "Canal quitté", - "Let others add options": "Permettre à d'autres d'ajouter des options", - "Limit votes per person": "Limiter les votes par personne", - "Link": "Lien", - "linkCount_one": "Lien", - "linkCount_many": "{{ count }} liens", - "linkCount_other": "{{ count }} liens", - "live": "en direct", - "Live for {{duration}}": "En direct pendant {{duration}}", - "Live location": "Emplacement en direct", - "Live until {{ timestamp }}": "En direct jusqu'à {{ timestamp }}", - "Load more": "Charger plus", - "Local upload attachment missing local id": "Pièce jointe locale sans identifiant local", - "Location": "Emplacement", - "Location sharing ended": "Partage d'emplacement terminé", - "Location: {{ coordinates }}": "Emplacement : {{ coordinates }}", - "Manage channel": "Gérer le canal", - "Manage members": "Gérer les membres", - "Mark as unread": "Marquer comme non lu", - "Maximum number of votes (from 2 to 10)": "Nombre maximum de votes (de 2 à 10)", - "Maximum votes per person": "Nombre maximal de votes par personne", - "Member detail": "Détails du membre", - "mention/Channel": "Canal", - "mention/Channel Description": "Notifier tout le monde dans ce canal", - "mention/Here": "Ici", - "mention/Here Description": "Notifier tous les membres en ligne dans ce canal", - "Menu": "Menu", - "Message deleted": "Message supprimé", - "Message Failed · Click to try again": "Échec de l'envoi du message - Cliquez pour réessayer", - "Message Failed · Unauthorized": "Échec de l'envoi du message - Non autorisé", - "Message failed to send": "Échec de l'envoi du message", - "Message has been successfully flagged": "Le message a été signalé avec succès", - "Message marked as unread": "Message marqué comme non lu", - "Message pinned": "Message épinglé", - "Message unpinned": "Message désépinglé", - "Message was blocked by moderation policies": "Le message a été bloqué par les politiques de modération", - "Messages have been marked unread.": "Les messages ont été marqués comme non lus.", - "Missing permissions to upload the attachment": "Autorisations manquantes pour télécharger la pièce jointe", - "Moderator": "Modérateur", - "Multiple votes": "Votes multiples", - "Mute": "Muet", - "Mute chat": "Mettre le chat en sourdine", - "Mute user": "Mettre l'utilisateur en sourdine", - "mute-command-args": "[@nomdutilisateur]", - "mute-command-description": "Muter un utilisateur", - "network error": "erreur réseau", - "New": "Nouveau", - "New message from {{user}}": "Nouveau message de {{user}}", - "New Messages!": "Nouveaux Messages!", - "Next": "Suivant", - "Next image": "Image suivante", - "No chats here yet…": "Pas encore de messages ici...", - "No conversations yet": "Aucune conversation pour le moment", - "No files": "Aucun fichier", - "No items exist": "Aucun élément", - "No member found": "Aucun membre trouvé", - "No messages found": "Aucun message trouvé", - "No photos or videos": "Aucune photo ni vidéo", - "No pinned messages": "Aucun message épinglé", - "No results found": "Aucun résultat trouvé", - "No user found": "Aucun utilisateur trouvé", - "Nobody will be able to vote in this poll anymore.": "Personne ne pourra plus voter dans ce sondage.", - "Nothing yet...": "Rien pour l'instant...", - "Notify all {{ role }} members": "Notifier tous les membres ayant le rôle {{ role }}", - "Offline": "Hors ligne", - "Ok": "D'accord", - "Online": "En ligne", - "Only numbers are allowed": "Seuls les chiffres sont autorisés", - "Only visible to you": "Visible uniquement pour vous", - "Open emoji picker": "Ouvrir le sélecteur d'émojis", - "Open gallery at image {{ index }}": "Ouvrir la galerie à l'image {{ index }}", - "Open image in gallery": "Ouvrir l'image dans la galerie", - "Open location in a map": "Ouvrir l'emplacement dans une carte", - "Open members actions": "Open members actions", - "Open menu": "Ouvrir le menu", - "Option already exists": "L'option existe déjà", - "Option is empty": "L'option est vide", - "Options": "Options", - "Original": "Original", - "Owner": "Propriétaire", - "People matching": "Correspondance de personnes", - "Photo": "Photo", - "Photos & videos": "Photos et vidéos", - "Pin": "Épingler", - "Pin a message to see it here": "Épinglez un message pour le voir ici", - "Pinned by {{ name }}": "Épinglé par {{ name }}", - "Pinned by You": "Épinglé par vous", - "Pinned message": "Message épinglé", - "Pinned messages": "Messages épinglés", - "placeholder/PollComment": "Votre commentaire", - "placeholder/PollOptionSuggestion": "Saisir une nouvelle option", - "Play video": "Lire la vidéo", - "Playback speed {{ rate }}x": "Vitesse de lecture {{ rate }}x", - "Poll": "Sondage", - "Poll comments": "Commentaires du sondage", - "Poll ended": "Sondage terminé", - "Poll options": "Options du sondage", - "Poll results": "Résultats du sondage", - "Poll sent": "Sondage envoyé", - "Previous": "Précédent", - "Previous image": "Image précédente", - "Question": "Question", - "Question {{ optionOrderNumber}}": "Question {{ optionOrderNumber}}", - "Question is required": "La question est obligatoire", - "Quote Reply": "Répondre par citation", - "Reached the vote limit. Remove an existing vote first.": "La limite de votes a été atteinte. Supprimez d'abord un vote existant.", - "Recording format is not supported and cannot be reproduced": "Le format d'enregistrement n'est pas pris en charge et ne peut pas être reproduit", - "Remind me": "Me rappeler", - "Remind Me": "Me rappeler", - "Reminder set": "Rappel défini", - "Remove": "Retirer", - "Remove {{ count }} members_one": "Retirer {{ count }} membre", - "Remove {{ count }} members_many": "Retirer {{ count }} membres", - "Remove {{ count }} members_other": "Retirer {{ count }} membres", - "Remove {{ member }} from this channel?": "Retirer {{ member }} de ce canal ?", - "Remove channel members": "Retirer les membres du canal", - "Remove reminder": "Supprimer le rappel", - "Remove save for later": "Supprimer « Enregistrer pour plus tard »", - "Remove user": "Retirer l'utilisateur", - "Removed {{ count }} members_one": "{{ count }} membre retiré", - "Removed {{ count }} members_many": "{{ count }} membres retirés", - "Removed {{ count }} members_other": "{{ count }} membres retirés", - "Replied to a thread": "A répondu à un fil", - "Reply": "Répondre", - "Reply to {{ authorName }}": "Répondre à {{ authorName }}", - "Reply to a message to start a thread": "Répondez à un message pour démarrer un fil", - "Reply to Message": "Répondre au message", - "replyCount_one": "1 réponse", - "replyCount_many": "{{ count }} réponses", - "replyCount_other": "{{ count }} réponses", - "Resend": "Renvoyer", - "Retry upload": "Réessayer le téléchargement", - "Review all options available in this poll": "Consultez toutes les options disponibles dans ce sondage", - "Review comments submitted with poll answers": "Consultez les commentaires envoyés avec les réponses au sondage", - "Review poll results and open an option to see detailed votes": "Consultez les résultats du sondage et ouvrez une option pour voir les votes détaillés", - "Review this message and choose whether to delete it, edit it, or send it anyway": "Consultez ce message et choisissez de le supprimer, le modifier ou l'envoyer quand même", - "Review who voted for this option": "Consultez qui a voté pour cette option", - "Save": "Enregistrer", - "Save for later": "Enregistrer pour plus tard", - "Saved for later": "Enregistré pour plus tard", - "Search": "Rechercher", - "Search GIFs": "Rechercher des GIFs", - "search-results-header-filter-source-button-label--channels": "canaux", - "search-results-header-filter-source-button-label--messages": "messages", - "search-results-header-filter-source-button-label--users": "utilisateurs", - "Searching for {{ searchSourceType }}...": "Recherche de {{ searchSourceType }}...", - "Searching...": "Recherche en cours...", - "searchResultsCount_one": "1 résultat", - "searchResultsCount_many": "{{ count }} résultats", - "searchResultsCount_other": "{{ count }} résultats", - "See all options ({{count}})_one": "Voir toutes les options ({{count}})", - "See all options ({{count}})_many": "Voir toutes les options ({{count}})", - "See all options ({{count}})_other": "Voir toutes les options ({{count}})", - "Select a thread to continue the conversation": "Sélectionnez un fil pour continuer la conversation", - "Select more than one option": "Sélectionner plus d'une option", - "Select one": "Sélectionner un", - "Select one or more": "Sélectionner un ou plusieurs", - "Select up to {{count}}_one": "Sélectionner jusqu'à {{count}}", - "Select up to {{count}}_many": "Sélectionner jusqu'à {{count}}", - "Select up to {{count}}_other": "Sélectionner jusqu'à {{count}}", - "Select your current location and optionally enable live location sharing": "Sélectionnez votre position actuelle et activez éventuellement le partage de position en direct", - "Send": "Envoyer", - "Send a message": "Envoyez un message", - "Send a message to start the conversation": "Envoyez un message pour commencer la conversation", - "Send Anyway": "Envoyer quand même", - "Send direct message": "Envoyer un message direct", - "Send message request failed": "Échec de la demande d'envoi de message", - "Send poll": "Envoyer le sondage", - "Sending...": "Envoi en cours...", - "Sent": "Envoyé", - "Share": "Partager", - "Share a file to see it here": "Partagez un fichier pour le voir ici", - "Share a photo or video to see it here": "Partagez une photo ou une vidéo pour la voir ici", - "Share live location for": "Partager l'emplacement en direct pendant", - "Share Location": "Partager l'emplacement", - "Shared live location": "Emplacement en direct partagé", - "Shared location": "Position partagée", - "Show all": "Tout afficher", - "Shuffle": "Mélanger", - "size limit": "limite de taille", - "Slow Mode ON": "Mode lent activé", - "Slow mode, wait {{ seconds }}s...": "Mode lent, attendez {{ seconds }} s...", - "Some of the files will not be accepted": "Certains fichiers ne seront pas acceptés", - "Start typing to search": "Commencez à taper pour rechercher", - "Stop sharing": "Arrêter de partager", - "Submit": "Envoyer", - "Suggest a new option to add to this poll": "Suggérez une nouvelle option à ajouter à ce sondage", - "Suggest an option": "Suggérer une option", - "Tap to remove": "Appuyez pour retirer", - "Tap to remove: {{ reactionName }}": "Appuyez pour retirer: {{ reactionName }}", - "Thinking...": "Réflexion...", - "this content could not be displayed": "ce contenu n'a pas pu être affiché", - "This field cannot be empty or contain only spaces": "Ce champ ne peut pas être vide ou contenir uniquement des espaces", - "This message did not meet our content guidelines": "Ce message ne respecte pas nos directives de contenu", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "Cet utilisateur pourra à nouveau vous envoyer des messages.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "Fil de discussion", - "Thread has not been found": "Le fil de discussion n'a pas été trouvé", - "Thread reply": "Réponse dans le fil", - "Thread Reply": "Réponse dans le fil", - "ThreadListUnseenThreadsBanner/loading": "Chargement...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} fil non lu", - "ThreadListUnseenThreadsBanner/unreadThreads_many": "{{ count }} fils non lus", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} fils non lus", - "Threads": "Fils", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Hier]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Aujourd'hui]\", \"nextDay\": \"[Demain]\", \"lastDay\": \"[Hier]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Dernier] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "il y a {{ count }} j", - "timestamp/relativeToday": "Aujourd'hui", - "timestamp/relativeWeeksAgo": "il y a {{ count }} sem", - "timestamp/relativeYesterday": "Hier", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Aujourd'hui] [à] HH:mm\", \"nextDay\": \"[Demain] [à] HH:mm\", \"lastDay\": \"[Hier] [à] HH:mm\", \"nextWeek\": \"dddd [à] HH:mm\", \"lastWeek\": \"dddd [dernier à] HH:mm\", \"sameElse\": \"ddd, D MMM [à] HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "Pour commencer l'enregistrement, autorisez l'accès à la caméra dans votre navigateur", - "To start recording, allow the microphone access in your browser": "Pour commencer l'enregistrement, autorisez l'accès au microphone dans votre navigateur", - "totalVoteCount_one": "1 vote au total", - "totalVoteCount_many": "{{ count }} votes au total", - "totalVoteCount_other": "{{ count }} votes au total", - "Translated": "Traduit", - "Translated from {{ language }}": "Traduit du {{ language }}", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "Tapez un nombre de 2 à 10", - "Type your message": "Tapez votre message", - "Unarchive": "Désarchiver", - "unban-command-args": "[@nomdutilisateur]", - "unban-command-description": "Débannir un utilisateur", - "Unblock": "Débloquer", - "Unblock user": "Débloquer l'utilisateur", - "Unblock User": "Débloquer l'utilisateur", - "unknown error": "erreur inconnue", - "Unmute": "Désactiver muet", - "Unmute chat": "Désactiver la sourdine du chat", - "Unmute user": "Désactiver la sourdine de l'utilisateur", - "unmute-command-args": "[@nomdutilisateur]", - "unmute-command-description": "Démuter un utilisateur", - "Unpin": "Détacher", - "Unread messages": "Messages non lus", - "Unsupported attachment": "Pièce jointe non prise en charge", - "unsupported file type": "type de fichier non pris en charge", - "Update": "Mettre à jour", - "Update the comment attached to your poll answer": "Mettez à jour le commentaire joint à votre réponse au sondage", - "Update your comment": "Mettre à jour votre commentaire", - "Upload blocked": "Téléversement bloqué", - "Upload error": "Erreur de téléversement", - "Upload failed": "Échec du téléversement", - "Upload Picture": "Importer une image", - "Upload type: \"{{ type }}\" is not allowed": "Le type de fichier : \"{{ type }}\" n'est pas autorisé", - "User blocked": "Utilisateur bloqué", - "User muted": "Utilisateur mis en sourdine", - "User removed": "Utilisateur retiré", - "User unblocked": "Utilisateur débloqué", - "User unmuted": "Sourdine de l'utilisateur désactivée", - "User uploaded content": "Contenu téléchargé par l'utilisateur", - "Video": "Vidéo", - "videoCount_one": "Vidéo", - "videoCount_many": "{{ count }} vidéos", - "videoCount_other": "{{ count }} vidéos", - "View": "Voir", - "View {{count}} comments_one": "Voir {{count}} commentaire", - "View {{count}} comments_many": "Voir {{count}} commentaires", - "View {{count}} comments_other": "Voir {{count}} commentaires", - "View all": "Tout voir", - "View member details for {{ member }}": "Voir les détails du membre {{ member }}", - "View original": "Voir l'original", - "View results": "Voir les résultats", - "View translation": "Voir la traduction", - "Voice message": "Message vocal", - "Voice message {{ duration }}": "Message vocal {{ duration }}", - "Voice message deleted": "Message vocal supprimé", - "voiceMessageCount_one": "Mémo vocal", - "voiceMessageCount_many": "{{ count }} mémos vocaux", - "voiceMessageCount_other": "{{ count }} mémos vocaux", - "Vote ended": "Vote terminé", - "Votes": "Votes", - "Wait until all attachments have uploaded": "Attendez que toutes les pièces jointes soient téléchargées", - "Waiting for network…": "En attente du réseau…", - "You": "Vous", - "You have no channels currently": "Vous n'avez actuellement aucun canal", - "You've reached the maximum number of files": "Vous avez atteint le nombre maximal de fichiers" -} diff --git a/src/i18n/hi.json b/src/i18n/hi.json deleted file mode 100644 index ea1f2ffa04..0000000000 --- a/src/i18n/hi.json +++ /dev/null @@ -1,709 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} और {{ moreCount }} और", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} और {{ lastUser }}", - "{{ count }} files_one": "{{ count }} फ़ाइल", - "{{ count }} files_other": "{{ count }} फ़ाइलें", - "{{ count }} members_one": "{{ count }} सदस्य", - "{{ count }} members_other": "{{ count }} सदस्य", - "{{ count }} members added_one": "{{ count }} सदस्य जोड़ा गया", - "{{ count }} members added_other": "{{ count }} सदस्य जोड़े गए", - "{{ count }} people are typing_one": "{{ count }} व्यक्ति टाइप कर रहा है", - "{{ count }} people are typing_many": "{{ count }} लोग टाइप कर रहे हैं", - "{{ count }} people are typing_other": "{{ count }} लोग टाइप कर रहे हैं", - "{{ count }} photos_one": "{{ count }} फ़ोटो", - "{{ count }} photos_other": "{{ count }} फ़ोटो", - "{{ count }} reactions_one": "{{ count }} प्रतिक्रिया", - "{{ count }} reactions_other": "{{ count }} प्रतिक्रियाएं", - "{{ count }} videos_one": "{{ count }} वीडियो", - "{{ count }} videos_other": "{{ count }} वीडियो", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} और {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }} और", - "{{ member }} will be able to message you again.": "{{ member }} आपको फिर से संदेश भेज सकेगा।", - "{{ member }} won't be able to message you anymore.": "{{ member }} अब आपको संदेश नहीं भेज सकेगा।", - "{{ memberCount }} members": "{{ memberCount }} मेंबर्स", - "{{ typing }} are typing": "{{ typing }} टाइप कर रहे हैं", - "{{ typing }} is typing": "{{ typing }} टाइप कर रहा है", - "{{ user }} has been muted": "{{ user }} को म्यूट कर दिया गया है", - "{{ user }} has been unmuted": "{{ user }} को अनम्यूट कर दिया गया है", - "{{ user }} is typing...": "{{ user }} टाइप कर रहा है...", - "{{ users }} and {{ user }} are typing...": "{{ users }} और {{ user }} टाइप कर रहे हैं...", - "{{ users }} and more are typing...": "{{ users }} और अधिक टाइप कर रहे हैं...", - "{{ watcherCount }} online": "{{ watcherCount }} ऑनलाइन", - "{{count}} new messages_one": "{{count}} नया संदेश", - "{{count}} new messages_other": "{{count}} नए संदेश", - "{{count}} unread_one": "{{count}} अपठित", - "{{count}} unread_other": "{{count}} अपठित", - "{{count}} votes_one": "{{count}} वोट", - "{{count}} votes_other": "{{count}} वोट", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+{{count}} और विकल्प", - "+{{count}} more options_other": "+{{count}} और विकल्प", - "🏙 Attachment...": "🏙 अटैचमेंट", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} ने बनाया: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} ने वोट दिया: {{pollOptionText}}", - "📍Shared location": "📍साझा किया गया स्थान", - "Actions": "Actions", - "Add": "जोड़ें", - "Add {{ count }} members_one": "{{ count }} सदस्य जोड़ें", - "Add {{ count }} members_other": "{{ count }} सदस्य जोड़ें", - "Add a comment": "एक टिप्पणी जोड़ें", - "Add a comment to your poll answer": "अपने पोल उत्तर में एक टिप्पणी जोड़ें", - "Add an option": "एक विकल्प जोड़ें", - "Add channel members": "चैनल सदस्य जोड़ें", - "Add members": "सदस्य जोड़ें", - "Add reaction": "प्रतिक्रिया जोड़ें", - "Admin": "एडमिन", - "All results loaded": "सभी परिणाम लोड हो गए", - "Allow access to camera": "कैमरा तक पहुँच दें", - "Allow access to microphone": "माइक्रोफ़ोन तक पहुँच दें", - "Allow comments": "टिप्पणियाँ की अनुमति दें", - "Allow option suggestion": "विकल्प सुझाव की अनुमति दें", - "Allow others to add comments": "दूसरों को टिप्पणी जोड़ने दें", - "Already a member": "पहले से सदस्य", - "Also send as a direct message": "सीधे संदेश के रूप में भी भेजें", - "Also send in channel": "चैनल में भी भेजें", - "Also sent in channel": "चैनल में भी भेजा गया", - "An error has occurred during recording": "रेकॉर्डिंग के दौरान एक त्रुटि आ गई है", - "An error has occurred during the recording processing": "रेकॉर्डिंग प्रोसेसिंग के दौरान एक त्रुटि आ गई है", - "Anonymous": "गुमनाम", - "Anonymous poll": "गुमनाम मतदान", - "Archive": "आर्काइव", - "Are you sure you want to delete this message?": "क्या आप वाकई इस संदेश को हटाना चाहते हैं?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} अटैचमेंट", - "aria/{{ count }} attachment_other": "{{ count }} अटैचमेंट", - "aria/{{ count }} search results_one": "{{ count }} खोज परिणाम", - "aria/{{ count }} search results_other": "{{ count }} खोज परिणाम", - "aria/{{ count }} suggestions_one": "{{ count }} सुझाव", - "aria/{{ count }} suggestions_other": "{{ count }} सुझाव", - "aria/{{ count }} unread message_one": "{{ count }} अपठित संदेश", - "aria/{{ count }} unread message_other": "{{ count }} अपठित संदेश", - "aria/{{ setting }} disabled": "{{ setting }} अक्षम किया गया", - "aria/{{ setting }} enabled": "{{ setting }} सक्षम किया गया", - "aria/Active": "सक्रिय", - "aria/Animated GIF": "एनिमेटेड GIF", - "aria/Animated GIF: {{ title }}": "एनिमेटेड GIF: {{ title }}", - "aria/Attachment": "अटैचमेंट", - "aria/Attachment {{ attachmentType }}": "अटैचमेंट {{ attachmentType }}", - "aria/Attachment Actions": "अटैचमेंट क्रियाएँ", - "aria/audio": "ऑडियो", - "aria/Audio position {{ elapsed }} of {{ duration }}": "ऑडियो स्थिति {{ elapsed }} / {{ duration }}", - "aria/Audio position {{ progress }} percent": "ऑडियो स्थिति {{ progress }} प्रतिशत", - "aria/Back to attachments": "अनुलग्नकों पर वापस जाएं", - "aria/Back to parent menu button": "मूल मेनू पर वापस जाएँ बटन", - "aria/Block User": "उपयोगकर्ता को ब्लॉक करें", - "aria/Bookmark Message": "संदेश बुकमार्क करें", - "aria/Cancel recording": "रिकॉर्डिंग रद्द करें", - "aria/Cancel Reply": "उत्तर रद्द करें", - "aria/Channel Actions": "चैनल क्रियाएँ", - "aria/Channel details": "चैनल विवरण", - "aria/Channel list": "चैनल सूची", - "aria/Chat view controls": "चैट व्यू नियंत्रण", - "aria/Chat: {{ channelName }}": "चैट: {{ channelName }}", - "aria/Clear search": "खोज साफ़ करें", - "aria/Close callout dialog": "कॉलआउट संवाद बंद करें", - "aria/Close thread": "थ्रेड बंद करें", - "aria/Collapse sidebar": "साइडबार संक्षिप्त करें", - "aria/Command activated: {{ command }}": "कमांड सक्रिय किया गया: {{ command }}", - "aria/Command Suggestions": "कमांड सुझाव", - "aria/Complete recording": "रिकॉर्डिंग पूर्ण करें", - "aria/Copy Message Text": "संदेश की टेक्स्ट कॉपी करें", - "aria/Decrease value": "मान घटाएं", - "aria/Delete Message": "संदेश डिलीट करें", - "aria/Delivered": "डिलीवर हो गया", - "aria/Delivery status: {{ deliveryStatus }}": "डिलीवरी स्थिति: {{ deliveryStatus }}", - "aria/Dismiss notification": "सूचना बंद करें", - "aria/Download attachment": "अनुलग्नक डाउनलोड करें", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "\"{{ option }}\" को स्थिति {{ position }} पर छोड़ा गया।", - "aria/Edit Message": "मैसेज में बदलाव करे", - "aria/Emoji picker": "इमोजी चुनने वाला", - "aria/Emoji Suggestions": "इमोजी सुझाव", - "aria/Exit search": "खोज से बाहर निकलें", - "aria/Expand sidebar": "साइडबार विस्तारित करें", - "aria/file": "फ़ाइल", - "aria/File upload": "फ़ाइल अपलोड", - "aria/Flag Message": "संदेश फ्लैग करें", - "aria/GIF": "GIF", - "aria/Giphy actions": "Giphy क्रियाएं", - "aria/Giphy canceled": "Giphy रद्द किया गया", - "aria/Giphy image changed": "Giphy छवि बदल गई", - "aria/Giphy image changed: {{ title }}": "Giphy छवि बदल गई: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Giphy पूर्वावलोकन, केवल आपको दिखाई देता है। भेजें, शफल करें या रद्द करें क्रियाओं का उपयोग करें।", - "aria/Giphy sent": "Giphy भेजा गया", - "aria/Go back": "वापस जाएं", - "aria/image": "छवि", - "aria/Image failed to load": "छवि लोड होने में विफल", - "aria/Increase value": "मान बढ़ाएं", - "aria/Jump to latest message": "नवीनतम संदेश पर जाएं", - "aria/Jump to quoted message": "उद्धृत संदेश पर जाएं", - "aria/Last activity: {{ time }}": "अंतिम गतिविधि: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "{{ sender }} का आखिरी संदेश: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "आखिरी संदेश: {{ messagePreview }}", - "aria/Mark Message Unread": "अपठित चिह्नित करें", - "aria/Mark messages as read": "संदेशों को पढ़ा हुआ चिह्नित करें", - "aria/Mention Suggestions": "उल्लेख सुझाव", - "aria/Message Actions": "संदेश कार्रवाइयाँ", - "aria/Message from {{ user }},": "{{ user }} का संदेश,", - "aria/Message input": "संदेश इनपुट", - "aria/Message with attachments": "अटैचमेंट के साथ संदेश", - "aria/Message,": "संदेश,", - "aria/Mute User": "उपयोगकर्ता म्यूट करें", - "aria/Next page": "अगला पृष्ठ", - "aria/No search results found": "कोई खोज परिणाम नहीं मिला", - "aria/Notifications": "सूचनाएं", - "aria/Open Attachment Selector": "अटैचमेंट चयनकर्ता खोलें", - "aria/Open Channel Actions Menu": "चैनल क्रियाएँ मेनू खोलें", - "aria/Open channel details": "चैनल विवरण खोलें", - "aria/Open channels view": "चैनल व्यू खोलें", - "aria/Open image shared by {{ name }}": "{{ name }} द्वारा साझा की गई छवि खोलें", - "aria/Open Message Actions Menu": "संदेश क्रिया मेन्यू खोलें", - "aria/Open Reaction Selector": "प्रतिक्रिया चयनकर्ता खोलें", - "aria/Open Thread": "थ्रेड खोलें", - "aria/Open threads view": "थ्रेड व्यू खोलें", - "aria/Open threads view with unread threads_one": "थ्रेड व्यू खोलें, {{ count }} अपठित थ्रेड", - "aria/Open threads view with unread threads_other": "थ्रेड व्यू खोलें, {{ count }} अपठित थ्रेड", - "aria/Open video shared by {{ name }}": "{{ name }} द्वारा साझा किया गया वीडियो खोलें", - "aria/Opened channel: {{ name }}": "चैनल खोला गया: {{ name }}", - "aria/Opened thread in {{ name }}": "{{ name }} में थ्रेड खोला गया", - "aria/Option {{ position }}": "विकल्प {{ position }}", - "aria/Options can now be reordered and removed.": "अब विकल्पों को पुनः क्रमित और हटाया जा सकता है।", - "aria/Pause": "रोकें", - "aria/Pause recording": "रिकॉर्डिंग रोकें", - "aria/Percent complete": "{{percent}} प्रतिशत पूर्ण", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "\"{{ option }}\" को उठाया गया। पुनर्व्यवस्थित करने के लिए तीर कुंजियों का उपयोग करें। छोड़ने के लिए स्पेस या टैब दबाएं।", - "aria/Pin Message": "संदेश पिन करें", - "aria/Play": "चलाएँ", - "aria/Poll dialog opened": "पोल संवाद खुला", - "aria/Poll sent": "पोल भेजा गया", - "aria/Poll: {{ pollName }}": "पोल: {{ pollName }}", - "aria/Press Enter to start typing": "टाइप करना शुरू करने के लिए Enter दबाएँ", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "इस विकल्प को चुनने के लिए स्पेस दबाएं, इसे स्थानांतरित करने के लिए ऊपर और नीचे तीर कुंजियों का उपयोग करें, फिर चयन हटाने के लिए फिर से स्पेस दबाएं।", - "aria/Previous page": "पिछला पृष्ठ", - "aria/Quote Message": "संदेश उद्धरण", - "aria/Reaction list": "प्रतिक्रिया सूची", - "aria/Read": "पढ़ा गया", - "aria/Recording paused": "रिकॉर्डिंग रोकी गई", - "aria/Recording resumed": "रिकॉर्डिंग फिर से शुरू हुई", - "aria/Recording started": "रिकॉर्डिंग शुरू हुई", - "aria/Remind Me Message": "मुझे याद दिलाएं", - "aria/Remove attachment": "संलग्नक हटाएं", - "aria/Remove location attachment": "स्थान संलग्नक हटाएं", - "aria/Remove option: {{ option }}": "विकल्प हटाएं: {{ option }}", - "aria/Remove Reminder": "अनुस्मारक हटाएं", - "aria/Remove Save For Later": "बाद में देखें हटाएं", - "aria/Removed option {{ option }}": "विकल्प {{ option }} हटाया गया", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "\"{{ option }}\" को पुनर्व्यवस्थित करें, {{ total }} में से स्थिति {{ position }}", - "aria/Reorder option {{ position }}": "विकल्प {{ position }} को पुनर्व्यवस्थित करें", - "aria/Resend Message": "संदेश फिर से भेजें", - "aria/Resume recording": "रिकॉर्डिंग फिर शुरू करें", - "aria/Retry upload": "अपलोड पुनः प्रयास करें", - "aria/Review bounced message": "वापस लौटा संदेश समीक्षा करें", - "aria/Search cleared": "खोज साफ़ की गई", - "aria/Search results": "खोज परिणाम", - "aria/Search results header filter button": "खोज परिणाम हेडर फ़िल्टर बटन", - "aria/Search results header filter button for: {{ source }}": "{{ source }} के लिए खोज परिणाम शीर्षक फ़िल्टर बटन", - "aria/Seek audio position": "ऑडियो स्थिति खोजें", - "aria/Select Reaction: {{ reactionName }}": "प्रतिक्रिया चुनें: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "उपयोगकर्ता चैनल चुनें: {{ name }}", - "aria/Send": "भेजें", - "aria/Sent": "भेजा गया", - "aria/Shared a link": "लिंक साझा किया गया", - "aria/Shared a link with title: {{ linkTitle }}": "शीर्षक के साथ लिंक साझा किया गया: {{ linkTitle }}", - "aria/Shared location": "साझा किया गया स्थान", - "aria/Show preview": "पूर्वावलोकन दिखाएं", - "aria/Start recording audio": "ऑडियो रिकॉर्डिंग शुरू करें", - "aria/Stop AI Generation": "एआई जनरेशन रोकें", - "aria/Submenu": "उप-मेन्यू", - "aria/Suggestions": "सुझाव", - "aria/There are no messages in this chat.": "इस चैट में कोई संदेश नहीं है", - "aria/This option can be reordered and removed.": "इस विकल्प को पुनः क्रमित और हटाया जा सकता है।", - "aria/Thread list": "थ्रेड सूची", - "aria/Thread: {{ messagePreview }}": "थ्रेड: {{ messagePreview }}", - "aria/Unblock User": "उपयोगकर्ता अनब्लॉक करें", - "aria/Unmute User": "उपयोगकर्ता अनम्यूट करें", - "aria/Unpin Message": "संदेश अनपिन करें", - "aria/User selected: {{ user }}": "उपयोगकर्ता चयनित: {{ user }}", - "aria/video": "वीडियो", - "aria/voice message": "वॉइस मैसेज", - "aria/Voice message sent": "वॉइस संदेश भेजा गया", - "aria/Voice recording attached": "वॉइस रिकॉर्डिंग संलग्न की गई", - "Ask a question": "एक प्रश्न पूछें", - "Attach": "संलग्न करें", - "Attach files": "फाइल्स अटैच करे", - "Attachment": "अनुलग्नक", - "Attachment upload blocked due to {{reason}}": "{{reason}} के कारण अटैचमेंट अपलोड ब्लॉक किया गया", - "Attachment upload failed due to {{reason}}": "{{reason}} के कारण अटैचमेंट अपलोड विफल रहा", - "Back": "वापस", - "ban-command-args": "[@उपयोगकर्तनाम] [पाठ]", - "ban-command-description": "एक उपयोगकर्ता को प्रतिषेधित करें", - "Block user": "उपयोगकर्ता को ब्लॉक करें", - "Block User": "उपयोगकर्ता को ब्लॉक करें", - "Browse channel members": "चैनल सदस्य देखें", - "Browse pinned messages": "पिन किए गए संदेश देखें", - "Cancel": "रद्द करें", - "Cannot seek in the recording": "रेकॉर्डिंग में खोज नहीं की जा सकती", - "Changes saved": "बदलाव सहेजे गए", - "Channel archived": "चैनल संग्रहीत किया गया", - "Channel members": "चैनल सदस्य", - "Channel Missing": "चैनल उपलब्ध नहीं है", - "Channel muted": "चैनल म्यूट किया गया", - "Channel pinned": "चैनल पिन किया गया", - "Channel unarchived": "चैनल असंग्रहीत किया गया", - "Channel unmuted": "चैनल अनम्यूट किया गया", - "Channel unpinned": "चैनल अनपिन किया गया", - "Channels": "चैनल", - "Chat deleted": "Chat deleted", - "Chats": "चैट", - "Choose between 2 to 10 options": "2 से 10 विकल्प चुनें", - "Close": "बंद करे", - "Close dialog": "डायलॉग बंद करें", - "Close emoji picker": "इमोजी पिकर बंद करें", - "Command not available while editing": "संपादन के दौरान कमांड उपलब्ध नहीं है", - "Command not available while replying": "उत्तर देते समय कमांड उपलब्ध नहीं है", - "Commands": "कमांड", - "Commands matching": "मेल खाती है", - "Connection failure, reconnecting now...": "कनेक्शन विफल रहा, अब पुनः कनेक्ट हो रहा है ...", - "Contact info": "संपर्क जानकारी", - "Contact name": "संपर्क का नाम", - "Copy Message": "संदेश कॉपी करें", - "Create": "बनाएँ", - "Create a question, add options, and configure poll settings": "एक प्रश्न बनाएं, विकल्प जोड़ें और पोल सेटिंग्स कॉन्फ़िगर करें", - "Create poll": "मतदान बनाएँ", - "Current location": "वर्तमान स्थान", - "Delete": "डिलीट", - "Delete chat": "चैट हटाएं", - "Delete for me": "मेरे लिए डिलीट करें", - "Delete message": "संदेश हटाएं", - "Delivered": "पहुंच गया", - "Direct message": "प्रत्यक्ष संदेश", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "क्या आप अभी इस पोल को समाप्त करना चाहते हैं? इस पोल में अब कोई भी वोट नहीं कर पाएगा।", - "Download {{ fileName }}": "{{ fileName }} डाउनलोड करें", - "Download All": "सब डाउनलोड करें", - "Download Attachment": "अटैचमेंट डाउनलोड करें", - "Download attachment {{ name }}": "अनुलग्नक {{ name }} डाउनलोड करें", - "Download attachment {{ number }}": "अनुलग्नक {{ number }} डाउनलोड करें", - "Drag your files here": "अपनी फ़ाइलें यहाँ खींचें", - "Drag your files here to add to your post": "अपनी फ़ाइलें यहाँ खींचें और अपने पोस्ट में जोड़ने के लिए", - "Due {{ timeLeft }}": "{{ timeLeft }} में देय", - "Due since {{ dueSince }}": "{{ dueSince }} से देय", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "संपादित करें", - "Edit chat data": "चैट डेटा संपादित करें", - "Edit contact": "संपर्क संपादित करें", - "Edit group": "समूह संपादित करें", - "Edit Message": "मैसेज में बदलाव करे", - "Edit message request failed": "संदेश संपादित करने का अनुरोध विफल रहा", - "Edited": "संपादित", - "Emoji matching": "इमोजी मिलान", - "Empty message...": "खाली संदेश ...", - "End": "समाप्त", - "End poll": "पोल समाप्त करें", - "End this poll?": "इस पोल को समाप्त करें?", - "End vote": "मत समाप्त करें", - "Enforce unique vote is enabled": "अनोखा वोट सक्षम है", - "Error": "त्रुटि", - "Error · Unsent": "फेल", - "Error adding flag": "ध्वज जोड़ने में त्रुटि", - "Error adding members": "Error adding members", - "Error blocking user": "उपयोगकर्ता को ब्लॉक करने में त्रुटि", - "Error connecting to chat, refresh the page to try again.": "चैट से कनेक्ट करने में त्रुटि, पेज को रिफ्रेश करें", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "संदेश हटाने में त्रुटि", - "Error fetching reactions": "प्रतिक्रियाएँ लोड करने में त्रुटि", - "Error marking message unread": "संदेश को अपठित चिह्नित करने में त्रुटि", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "संदेश को अपठित मार्क करने में त्रुटि। सबसे नए 100 चैनल संदेश से पहले के सभी अपठित संदेशों को अपठित मार्क नहीं किया जा सकता है।", - "Error muting a user ...": "यूजर को म्यूट करने का प्रयास फेल हुआ", - "Error muting channel": "चैनल म्यूट करने में त्रुटि", - "Error muting user": "उपयोगकर्ता को म्यूट करने में त्रुटि", - "Error opening direct message": "सीधा संदेश खोलने में त्रुटि", - "Error pinning message": "संदेश को पिन करने में त्रुटि", - "Error removing members": "सदस्यों को हटाने में त्रुटि", - "Error removing message pin": "संदेश पिन निकालने में त्रुटि", - "Error removing user": "उपयोगकर्ता हटाने में त्रुटि", - "Error reproducing the recording": "रिकॉर्डिंग पुन: उत्पन्न करने में त्रुटि", - "Error starting recording": "रेकॉर्डिंग शुरू करने में त्रुटि", - "Error unblocking user": "उपयोगकर्ता को अनब्लॉक करने में त्रुटि", - "Error unmuting a user ...": "यूजर को अनम्यूट करने का प्रयास फेल हुआ", - "Error unmuting channel": "चैनल को अनम्यूट करने में त्रुटि", - "Error unmuting user": "उपयोगकर्ता को अनम्यूट करने में त्रुटि", - "Error uploading attachment": "अटैचमेंट अपलोड करते समय त्रुटि", - "Error uploading file": "फ़ाइल अपलोड करने में त्रुटि", - "Error uploading image": "छवि अपलोड करने में त्रुटि", - "Error: {{ errorMessage }}": "फेल: {{ errorMessage }}", - "Exit command {{ command }}": "कमांड से बाहर निकलें {{ command }}", - "Failed to block user": "उपयोगकर्ता को ब्लॉक करने में विफल", - "Failed to create the poll": "मतदान बनाने में विफल", - "Failed to create the poll due to {{reason}}": "मतदान {{reason}} के कारण नहीं बन सका", - "Failed to delete the message": "संदेश हटाने में विफल", - "Failed to end the poll": "पोल समाप्त करने में विफल", - "Failed to end the poll due to {{reason}}": "{{reason}} के कारण पोल समाप्त करने में विफल", - "Failed to jump to the first unread message": "पहले अपठित संदेश पर जाने में विफल", - "Failed to leave channel": "चैनल छोड़ने में विफल", - "Failed to load channels": "चैनल लोड करने में विफल", - "Failed to load more channels": "और चैनल लोड करने में विफल", - "Failed to mark channel as read": "चैनल को पढ़ा हुआ चिह्नित करने में विफल।", - "Failed to play the recording": "रेकॉर्डिंग प्ले करने में विफल", - "Failed to retrieve location": "स्थान प्राप्त करने में विफल", - "Failed to save changes": "बदलाव सहेजने में विफल", - "Failed to share location": "स्थान साझा करने में विफल", - "Failed to update channel archive status": "चैनल के आर्काइव स्थिति को अपडेट करने में विफल", - "Failed to update channel mute status": "चैनल की म्यूट स्थिति को अपडेट करने में विफल", - "Failed to update channel pinned status": "चैनल की पिन स्थिति को अपडेट करने में विफल", - "File": "फ़ाइल", - "File is required for upload attachment": "अटैचमेंट अपलोड के लिए फ़ाइल आवश्यक है", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "फ़ाइल बहुत बड़ी है: {{ size }}, अधिकतम अपलोड साइज़ {{ limit }} है", - "File too large": "फ़ाइल बहुत बड़ी है", - "fileCount_one": "1 फ़ाइल", - "fileCount_other": "{{ count }} फ़ाइलें", - "Files": "फ़ाइलें", - "Flag": "फ्लैग करे", - "Generating...": "बना रहा है...", - "giphy-command-args": "[पाठ]", - "giphy-command-description": "चैनल पर एक क्रॉफिल जीआइएफ पोस्ट करें", - "Go back": "वापस जाएं", - "Group info": "समूह जानकारी", - "Group name": "समूह का नाम", - "Hide who voted": "किसने वोट दिया छिपाएं", - "Image": "छवि", - "imageCount_one": "1 छवि", - "imageCount_other": "{{ count }} छवियाँ", - "Instant commands": "तत्काल कमांड", - "language/af": "अफ्रीकी", - "language/am": "अम्हारिक", - "language/ar": "अरबी", - "language/az": "अज़रबैजानी", - "language/bg": "बुल्गारियाई", - "language/bn": "बंगाली", - "language/bs": "बोस्नियाई", - "language/cs": "चेक", - "language/da": "डेनिश", - "language/de": "जर्मन", - "language/el": "यूनानी", - "language/en": "अंग्रेज़ी", - "language/es": "स्पेनिश", - "language/es-MX": "स्पेनिश (मेक्सिको)", - "language/et": "एस्तोनियाई", - "language/fa": "फ़ारसी", - "language/fa-AF": "दरी", - "language/fi": "फ़िनिश", - "language/fr": "फ़्रेंच", - "language/fr-CA": "फ़्रेंच (कनाडा)", - "language/ha": "हौसा", - "language/he": "हिब्रू", - "language/hi": "हिंदी", - "language/hr": "क्रोएशियाई", - "language/ht": "हैतियाई क्रियोल", - "language/hu": "हंगेरी", - "language/id": "इंडोनेशियाई", - "language/it": "इतालवी", - "language/ja": "जापानी", - "language/ka": "जॉर्जियाई", - "language/ko": "कोरियाई", - "language/lt": "लिथुआनियाई", - "language/lv": "लातवियाई", - "language/ms": "मलय", - "language/nl": "डच", - "language/no": "नॉर्वेजियाई", - "language/pl": "पोलिश", - "language/ps": "पश्तो", - "language/pt": "पुर्तगाली", - "language/ro": "रोमानियाई", - "language/ru": "रूसी", - "language/sk": "स्लोवाक", - "language/sl": "स्लोवेनियाई", - "language/so": "सोमाली", - "language/sq": "अल्बानियाई", - "language/sr": "सर्बियाई", - "language/sv": "स्वीडिश", - "language/sw": "स्वाहिली", - "language/ta": "तमिल", - "language/th": "थाई", - "language/tl": "तागालोग", - "language/tr": "तुर्की", - "language/uk": "यूक्रेनियाई", - "language/ur": "उर्दू", - "language/vi": "वियतनामी", - "language/zh": "चीनी (सरलीकृत)", - "language/zh-TW": "चीनी (पारंपरिक)", - "Last seen {{ timestamp }}": "अंतिम बार {{ timestamp }} देखा गया", - "Leave Channel": "चैनल छोड़ें", - "Leave chat": "चैनल छोड़ें", - "Left channel": "चैनल छोड़ दिया गया", - "Let others add options": "दूसरों को विकल्प जोड़ने दें", - "Limit votes per person": "प्रति व्यक्ति वोट सीमित करें", - "Link": "लिंक", - "linkCount_one": "1 लिंक", - "linkCount_other": "{{ count }} लिंक", - "live": "लाइव", - "Live for {{duration}}": "{{duration}} के लिए लाइव", - "Live location": "लाइव स्थान", - "Live until {{ timestamp }}": "{{ timestamp }} तक लाइव", - "Load more": "और लोड करें", - "Local upload attachment missing local id": "लोकल अपलोड अटैचमेंट में लोकल आईडी नहीं है", - "Location": "स्थान", - "Location sharing ended": "स्थान साझा करना समाप्त", - "Location: {{ coordinates }}": "स्थान: {{ coordinates }}", - "Manage channel": "चैनल प्रबंधित करें", - "Manage members": "सदस्य प्रबंधित करें", - "Mark as unread": "अपठित चिह्नित करें", - "Maximum number of votes (from 2 to 10)": "अधिकतम वोटों की संख्या (2 से 10)", - "Maximum votes per person": "प्रति व्यक्ति अधिकतम वोट", - "Member detail": "सदस्य विवरण", - "mention/Channel": "चैनल", - "mention/Channel Description": "इस चैनल में सभी को सूचित करें", - "mention/Here": "यहां", - "mention/Here Description": "इस चैनल के सभी ऑनलाइन सदस्यों को सूचित करें", - "Menu": "मेन्यू", - "Message deleted": "मैसेज हटा दिया गया", - "Message Failed · Click to try again": "मैसेज फ़ैल - पुनः कोशिश करें", - "Message Failed · Unauthorized": "मैसेज फ़ैल - अनधिकृत", - "Message failed to send": "संदेश भेजने में विफल", - "Message has been successfully flagged": "मैसेज को फ्लैग कर दिया गया है", - "Message marked as unread": "संदेश को अपठित के रूप में चिह्नित किया गया", - "Message pinned": "संदेश पिन किया गया", - "Message unpinned": "संदेश अनपिन किया गया", - "Message was blocked by moderation policies": "संदेश को मॉडरेशन नीतियों द्वारा ब्लॉक कर दिया गया है", - "Messages have been marked unread.": "संदेशों को अपठित चिह्नित किया गया है।", - "Missing permissions to upload the attachment": "अटैचमेंट अपलोड करने के लिए अनुमतियां गायब", - "Moderator": "मॉडरेटर", - "Multiple votes": "कई वोट", - "Mute": "म्यूट करे", - "Mute chat": "चैट म्यूट करें", - "Mute user": "उपयोगकर्ता को म्यूट करें", - "mute-command-args": "[@उपयोगकर्तनाम]", - "mute-command-description": "एक उपयोगकर्ता को म्यूट करें", - "network error": "नेटवर्क त्रुटि", - "New": "नए", - "New message from {{user}}": "{{user}} से नया संदेश", - "New Messages!": "नए मैसेज!", - "Next": "अगला", - "Next image": "अगली छवि", - "No chats here yet…": "यहां अभी तक कोई चैट नहीं...", - "No conversations yet": "अभी तक कोई बातचीत नहीं है", - "No files": "कोई फ़ाइल नहीं", - "No items exist": "कोई आइटम मौजूद नहीं है", - "No member found": "कोई सदस्य नहीं मिला", - "No messages found": "कोई संदेश नहीं मिला", - "No photos or videos": "कोई फ़ोटो या वीडियो नहीं", - "No pinned messages": "कोई पिन किया गया संदेश नहीं", - "No results found": "कोई परिणाम नहीं मिला", - "No user found": "कोई उपयोगकर्ता नहीं मिला", - "Nobody will be able to vote in this poll anymore.": "अब कोई भी इस मतदान में मतदान नहीं कर सकेगा।", - "Nothing yet...": "कोई मैसेज नहीं है", - "Notify all {{ role }} members": "{{ role }} भूमिका वाले सभी सदस्यों को सूचित करें", - "Offline": "ऑफलाइन", - "Ok": "ठीक है", - "Online": "ऑनलाइन", - "Only numbers are allowed": "केवल संख्याएँ अनुमत हैं", - "Only visible to you": "केवल आपको दिखाई देता है", - "Open emoji picker": "इमोजी पिकर खोलिये", - "Open gallery at image {{ index }}": "गैलरी को छवि {{ index }} पर खोलें", - "Open image in gallery": "छवि को गैलरी में खोलें", - "Open location in a map": "मानचित्र में स्थान खोलें", - "Open members actions": "Open members actions", - "Open menu": "मेन्यू खोलें", - "Option already exists": "विकल्प पहले से मौजूद है", - "Option is empty": "विकल्प खाली है", - "Options": "विकल्प", - "Original": "मूल", - "Owner": "मालिक", - "People matching": "मेल खाते लोग", - "Photo": "फ़ोटो", - "Photos & videos": "फ़ोटो और वीडियो", - "Pin": "पिन", - "Pin a message to see it here": "इसे यहाँ देखने के लिए संदेश पिन करें", - "Pinned by {{ name }}": "{{ name }} द्वारा पिन किया गया", - "Pinned by You": "आपके द्वारा पिन किया गया", - "Pinned message": "पिन किया गया संदेश", - "Pinned messages": "पिन किए गए संदेश", - "placeholder/PollComment": "आपकी टिप्पणी", - "placeholder/PollOptionSuggestion": "नया विकल्प दर्ज करें", - "Play video": "वीडियो चलाएं", - "Playback speed {{ rate }}x": "प्लेबैक गति {{ rate }}x", - "Poll": "मतदान", - "Poll comments": "मतदान टिप्पणियाँ", - "Poll ended": "पोल समाप्त", - "Poll options": "मतदान विकल्प", - "Poll results": "मतदान परिणाम", - "Poll sent": "पोल भेजा गया", - "Previous": "पिछला", - "Previous image": "पिछली छवि", - "Question": "प्रश्न", - "Question {{ optionOrderNumber}}": "प्रश्न {{ optionOrderNumber}}", - "Question is required": "प्रश्न आवश्यक है", - "Quote Reply": "उद्धरण जवाब", - "Reached the vote limit. Remove an existing vote first.": "मतदान सीमा तक पहुंच गया। पहले एक मौजूदा वोट हटाएं।", - "Recording format is not supported and cannot be reproduced": "रेकॉर्डिंग फ़ॉर्मेट समर्थित नहीं है और पुनः उत्पन्न नहीं किया जा सकता", - "Remind me": "मुझे याद दिलाएं", - "Remind Me": "मुझे याद दिलाएं", - "Reminder set": "अनुस्मारक सेट किया गया", - "Remove": "हटाएं", - "Remove {{ count }} members_one": "{{ count }} सदस्य हटाएं", - "Remove {{ count }} members_other": "{{ count }} सदस्य हटाएं", - "Remove {{ member }} from this channel?": "इस चैनल से {{ member }} को हटाएं?", - "Remove channel members": "चैनल सदस्य हटाएं", - "Remove reminder": "रिमाइंडर हटाएं", - "Remove save for later": "बाद में देखें हटाएं", - "Remove user": "उपयोगकर्ता हटाएं", - "Removed {{ count }} members_one": "{{ count }} सदस्य हटाया गया", - "Removed {{ count }} members_other": "{{ count }} सदस्य हटाए गए", - "Replied to a thread": "थ्रेड में जवाब दिया", - "Reply": "जवाब दे दो", - "Reply to {{ authorName }}": "{{ authorName }} को जवाब दें", - "Reply to a message to start a thread": "थ्रेड शुरू करने के लिए किसी संदेश का जवाब दें", - "Reply to Message": "संदेश का जवाब दें", - "replyCount_one": "1 रिप्लाई", - "replyCount_other": "{{ count }} रिप्लाई", - "Resend": "फिर से भेजें", - "Retry upload": "अपलोड फिर से करें", - "Review all options available in this poll": "इस पोल में उपलब्ध सभी विकल्पों की समीक्षा करें", - "Review comments submitted with poll answers": "पोल उत्तरों के साथ भेजी गई टिप्पणियों की समीक्षा करें", - "Review poll results and open an option to see detailed votes": "पोल परिणामों की समीक्षा करें और विस्तृत वोट देखने के लिए एक विकल्प खोलें", - "Review this message and choose whether to delete it, edit it, or send it anyway": "इस संदेश की समीक्षा करें और चुनें कि इसे हटाना है, संपादित करना है या फिर भी भेजना है", - "Review who voted for this option": "समीक्षा करें कि इस विकल्प के लिए किसने वोट किया", - "Save": "सहेजें", - "Save for later": "बाद के लिए सहेजें", - "Saved for later": "बाद के लिए सहेजा गया", - "Search": "खोज", - "Search GIFs": "GIF खोजें", - "search-results-header-filter-source-button-label--channels": "चैनल्स", - "search-results-header-filter-source-button-label--messages": "संदेश", - "search-results-header-filter-source-button-label--users": "उपयोगकर्ता", - "Searching for {{ searchSourceType }}...": "{{ searchSourceType }} खोज रहे हैं...", - "Searching...": "खोज कर...", - "searchResultsCount_one": "1 परिणाम", - "searchResultsCount_other": "{{ count }} परिणाम", - "See all options ({{count}})_one": "सभी विकल्प देखें ({{count}})", - "See all options ({{count}})_other": "सभी विकल्प देखें ({{count}})", - "Select a thread to continue the conversation": "बातचीत जारी रखने के लिए एक थ्रेड चुनें", - "Select more than one option": "एक से अधिक विकल्प चुनें", - "Select one": "एक चुनें", - "Select one or more": "एक या अधिक चुनें", - "Select up to {{count}}_one": "अधिकतम {{count}} तक चुनें", - "Select up to {{count}}_other": "अधिकतम {{count}} तक चुनें", - "Select your current location and optionally enable live location sharing": "अपना वर्तमान स्थान चुनें और वैकल्पिक रूप से लाइव लोकेशन शेयरिंग सक्षम करें", - "Send": "भेजे", - "Send a message": "संदेश भेजें", - "Send a message to start the conversation": "बातचीत शुरू करने के लिए संदेश भेजें", - "Send Anyway": "वैसे भी भेजें", - "Send direct message": "सीधा संदेश भेजें", - "Send message request failed": "संदेश भेजने का अनुरोध विफल रहा", - "Send poll": "पोल भेजें", - "Sending...": "भेजा जा रहा है", - "Sent": "भेजा गया", - "Share": "साझा करें", - "Share a file to see it here": "इसे यहाँ देखने के लिए एक फ़ाइल साझा करें", - "Share a photo or video to see it here": "इसे यहाँ देखने के लिए एक फ़ोटो या वीडियो साझा करें", - "Share live location for": "लाइव स्थान साझा करें", - "Share Location": "स्थान साझा करें", - "Shared live location": "साझा किया गया लाइव स्थान", - "Shared location": "साझा स्थान", - "Show all": "सभी दिखाएँ", - "Shuffle": "मिश्रित करें", - "size limit": "आकार सीमा", - "Slow Mode ON": "स्लो मोड ऑन", - "Slow mode, wait {{ seconds }}s...": "स्लो मोड, {{ seconds }} सेकंड प्रतीक्षा करें...", - "Some of the files will not be accepted": "कुछ फ़ाइलें स्वीकार नहीं की जाएंगी", - "Start typing to search": "खोजने के लिए टाइप करना शुरू करें", - "Stop sharing": "साझा करना बंद करें", - "Submit": "जमा करें", - "Suggest a new option to add to this poll": "इस पोल में जोड़ने के लिए एक नया विकल्प सुझाएं", - "Suggest an option": "एक विकल्प सुझाव दें", - "Tap to remove": "हटाने के लिए टैप करें", - "Tap to remove: {{ reactionName }}": "हटाने के लिए टैप करें: {{ reactionName }}", - "Thinking...": "सोच रहा है...", - "this content could not be displayed": "यह कॉन्टेंट लोड नहीं हो पाया", - "This field cannot be empty or contain only spaces": "यह फ़ील्ड खाली नहीं हो सकता या केवल रिक्त स्थान नहीं रख सकता", - "This message did not meet our content guidelines": "यह संदेश हमारे सामग्री दिशानिर्देशों के अनुरूप नहीं था", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "यह उपयोगकर्ता आपको फिर से संदेश भेज सकेगा।", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "रिप्लाई थ्रेड", - "Thread has not been found": "थ्रेड नहीं मिला", - "Thread reply": "थ्रेड में उत्तर", - "Thread Reply": "थ्रेड में उत्तर", - "ThreadListUnseenThreadsBanner/loading": "लोड हो रहा है...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} अपठित थ्रेड", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} अपठित थ्रेड", - "Threads": "थ्रेड्स", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[बीता कल]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[आज]\", \"nextDay\": \"[कल]\", \"lastDay\": \"[बीता कल]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[पिछला] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "{{ count }} दिन पहले", - "timestamp/relativeToday": "आज", - "timestamp/relativeWeeksAgo": "{{ count }} सप्ताह पहले", - "timestamp/relativeYesterday": "बीता कल", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[आज] HH:mm\", \"nextDay\": \"[कल] HH:mm\", \"lastDay\": \"[बीता कल] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[पिछले] dddd, HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "रिकॉर्डिंग शुरू करने के लिए, अपने ब्राउज़र में कैमरा तक पहुँच दें", - "To start recording, allow the microphone access in your browser": "रिकॉर्डिंग शुरू करने के लिए, अपने ब्राउज़र में माइक्रोफ़ोन तक पहुँच दें", - "totalVoteCount_one": "कुल 1 वोट", - "totalVoteCount_other": "कुल {{ count }} वोट", - "Translated": "अनुवादित", - "Translated from {{ language }}": "{{ language }} से अनुवादित", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "2 से 10 तक का एक नंबर टाइप करें", - "Type your message": "अपना मैसेज लिखे", - "Unarchive": "अनआर्काइव", - "unban-command-args": "[@उपयोगकर्तनाम]", - "unban-command-description": "एक उपयोगकर्ता को प्रतिषेध से मुक्त करें", - "Unblock": "अनब्लॉक करें", - "Unblock user": "उपयोगकर्ता अनब्लॉक करें", - "Unblock User": "उपयोगकर्ता अनब्लॉक करें", - "unknown error": "अज्ञात त्रुटि", - "Unmute": "अनम्यूट", - "Unmute chat": "चैट अनम्यूट करें", - "Unmute user": "उपयोगकर्ता को अनम्यूट करें", - "unmute-command-args": "[@उपयोगकर्तनाम]", - "unmute-command-description": "एक उपयोगकर्ता को अनम्यूट करें", - "Unpin": "अनपिन", - "Unread messages": "अपठित संदेश", - "Unsupported attachment": "असमर्थित अटैचमेंट", - "unsupported file type": "असमर्थित फ़ाइल प्रकार", - "Update": "अपडेट करें", - "Update the comment attached to your poll answer": "अपने पोल उत्तर से जुड़ी टिप्पणी अपडेट करें", - "Update your comment": "अपने टिप्पणी को अपडेट करें", - "Upload blocked": "अपलोड अवरुद्ध", - "Upload error": "अपलोड त्रुटि", - "Upload failed": "अपलोड विफल", - "Upload Picture": "चित्र अपलोड करें", - "Upload type: \"{{ type }}\" is not allowed": "अपलोड प्रकार: \"{{ type }}\" की अनुमति नहीं है", - "User blocked": "उपयोगकर्ता अवरुद्ध किया गया", - "User muted": "उपयोगकर्ता म्यूट किया गया", - "User removed": "उपयोगकर्ता हटा दिया गया", - "User unblocked": "उपयोगकर्ता अनब्लॉक किया गया", - "User unmuted": "उपयोगकर्ता अनम्यूट किया गया", - "User uploaded content": "उपयोगकर्ता अपलोड की गई सामग्री", - "Video": "वीडियो", - "videoCount_one": "1 वीडियो", - "videoCount_other": "{{ count }} वीडियो", - "View": "देखें", - "View {{count}} comments_one": "देखें {{count}} टिप्पणी", - "View {{count}} comments_other": "देखें {{count}} टिप्पणियाँ", - "View all": "सभी देखें", - "View member details for {{ member }}": "{{ member }} के सदस्य विवरण देखें", - "View original": "मूल देखें", - "View results": "परिणाम देखें", - "View translation": "अनुवाद देखें", - "Voice message": "आवाज संदेश", - "Voice message {{ duration }}": "वॉइस संदेश {{ duration }}", - "Voice message deleted": "वॉइस संदेश हटा दिया गया", - "voiceMessageCount_one": "1 ध्वनि संदेश", - "voiceMessageCount_other": "{{ count }} ध्वनि संदेश", - "Vote ended": "मतदान समाप्त", - "Votes": "वोट", - "Wait until all attachments have uploaded": "सभी अटैचमेंट अपलोड होने तक प्रतीक्षा करें", - "Waiting for network…": "नेटवर्क की प्रतीक्षा…", - "You": "आप", - "You have no channels currently": "आपके पास कोई चैनल नहीं है", - "You've reached the maximum number of files": "आप अधिकतम फ़ाइलों तक पहुँच गए हैं" -} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 4d3f5a0cf5..174f239db6 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,4 +1,3 @@ -export * from './translations'; export * from './Streami18n'; export * from './TranslationBuilder'; export { diff --git a/src/i18n/it.json b/src/i18n/it.json deleted file mode 100644 index e7db30cfab..0000000000 --- a/src/i18n/it.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} e altri {{ moreCount }}", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} e {{ lastUser }}", - "{{ count }} files_one": "{{ count }} file", - "{{ count }} files_many": "{{ count }} file", - "{{ count }} files_other": "{{ count }} file", - "{{ count }} members_one": "{{ count }} membro", - "{{ count }} members_many": "{{ count }} membri", - "{{ count }} members_other": "{{ count }} membri", - "{{ count }} members added_one": "{{ count }} membro aggiunto", - "{{ count }} members added_many": "{{ count }} membri aggiunti", - "{{ count }} members added_other": "{{ count }} membri aggiunti", - "{{ count }} people are typing_one": "{{ count }} persona sta scrivendo", - "{{ count }} people are typing_many": "{{ count }} persone stanno scrivendo", - "{{ count }} people are typing_other": "{{ count }} persone stanno scrivendo", - "{{ count }} photos_one": "{{ count }} foto", - "{{ count }} photos_many": "{{ count }} foto", - "{{ count }} photos_other": "{{ count }} foto", - "{{ count }} reactions_one": "{{ count }} reazione", - "{{ count }} reactions_many": "{{ count }} reazioni", - "{{ count }} reactions_other": "{{ count }} reazioni", - "{{ count }} videos_one": "{{ count }} video", - "{{ count }} videos_many": "{{ count }} video", - "{{ count }} videos_other": "{{ count }} video", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} e {{ secondUser }}", - "{{ imageCount }} more": "+ {{ imageCount }}", - "{{ member }} will be able to message you again.": "{{ member }} potrà inviarti di nuovo messaggi.", - "{{ member }} won't be able to message you anymore.": "{{ member }} non potrà più inviarti messaggi.", - "{{ memberCount }} members": "{{ memberCount }} membri", - "{{ typing }} are typing": "{{ typing }} stanno scrivendo", - "{{ typing }} is typing": "{{ typing }} sta scrivendo", - "{{ user }} has been muted": "{{ user }} è stato silenziato", - "{{ user }} has been unmuted": "Notifiche riattivate per {{ user }}", - "{{ user }} is typing...": "{{ user }} sta digitando...", - "{{ users }} and {{ user }} are typing...": "{{ users }} e {{ user }} stanno digitando...", - "{{ users }} and more are typing...": "{{ users }} e altri stanno digitando...", - "{{ watcherCount }} online": "{{ watcherCount }} online", - "{{count}} new messages_one": "{{count}} nuovo messaggio", - "{{count}} new messages_many": "{{count}} nuovi messaggi", - "{{count}} new messages_other": "{{count}} nuovi messaggi", - "{{count}} unread_one": "{{count}} non letto", - "{{count}} unread_many": "{{count}} non letti", - "{{count}} unread_other": "{{count}} non letti", - "{{count}} votes_one": "{{count}} voto", - "{{count}} votes_many": "{{count}} voti", - "{{count}} votes_other": "{{count}} voti", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+{{count}} altra opzione", - "+{{count}} more options_many": "+{{count}} altre opzioni", - "+{{count}} more options_other": "+{{count}} altre opzioni", - "🏙 Attachment...": "🏙 Allegato...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} ha creato: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} ha votato: {{pollOptionText}}", - "📍Shared location": "📍Posizione condivisa", - "Actions": "Actions", - "Add": "Aggiungi", - "Add {{ count }} members_one": "Aggiungi {{ count }} membro", - "Add {{ count }} members_many": "Aggiungi {{ count }} membri", - "Add {{ count }} members_other": "Aggiungi {{ count }} membri", - "Add a comment": "Aggiungi un commento", - "Add a comment to your poll answer": "Aggiungi un commento alla tua risposta al sondaggio", - "Add an option": "Aggiungi un'opzione", - "Add channel members": "Aggiungi membri al canale", - "Add members": "Aggiungi membri", - "Add reaction": "Aggiungi reazione", - "Admin": "Admin", - "All results loaded": "Tutti i risultati caricati", - "Allow access to camera": "Consenti l'accesso alla fotocamera", - "Allow access to microphone": "Consenti l'accesso al microfono", - "Allow comments": "Consenti i commenti", - "Allow option suggestion": "Consenti il suggerimento di opzioni", - "Allow others to add comments": "Consenti ad altri di aggiungere commenti", - "Already a member": "Già membro", - "Also send as a direct message": "Invia anche come messaggio diretto", - "Also send in channel": "Invia anche nel canale", - "Also sent in channel": "Inviato anche nel canale", - "An error has occurred during recording": "Si è verificato un errore durante la registrazione", - "An error has occurred during the recording processing": "Si è verificato un errore durante l'elaborazione della registrazione", - "Anonymous": "Anonimo", - "Anonymous poll": "Sondaggio anonimo", - "Archive": "Archivia", - "Are you sure you want to delete this message?": "Sei sicuro di voler eliminare questo messaggio?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_many": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} allegato", - "aria/{{ count }} attachment_many": "{{ count }} allegati", - "aria/{{ count }} attachment_other": "{{ count }} allegati", - "aria/{{ count }} search results_one": "{{ count }} risultato di ricerca", - "aria/{{ count }} search results_many": "{{ count }} risultati di ricerca", - "aria/{{ count }} search results_other": "{{ count }} risultati di ricerca", - "aria/{{ count }} suggestions_one": "{{ count }} suggerimento", - "aria/{{ count }} suggestions_many": "{{ count }} suggerimenti", - "aria/{{ count }} suggestions_other": "{{ count }} suggerimenti", - "aria/{{ count }} unread message_one": "{{ count }} messaggio non letto", - "aria/{{ count }} unread message_many": "{{ count }} messaggi non letti", - "aria/{{ count }} unread message_other": "{{ count }} messaggi non letti", - "aria/{{ setting }} disabled": "{{ setting }} disattivato", - "aria/{{ setting }} enabled": "{{ setting }} attivato", - "aria/Active": "Attivo", - "aria/Animated GIF": "GIF animata", - "aria/Animated GIF: {{ title }}": "GIF animata: {{ title }}", - "aria/Attachment": "Allegato", - "aria/Attachment {{ attachmentType }}": "Allegato {{ attachmentType }}", - "aria/Attachment Actions": "Azioni allegato", - "aria/audio": "audio", - "aria/Audio position {{ elapsed }} of {{ duration }}": "Posizione audio {{ elapsed }} di {{ duration }}", - "aria/Audio position {{ progress }} percent": "Posizione audio {{ progress }} percento", - "aria/Back to attachments": "Torna agli allegati", - "aria/Back to parent menu button": "Torna al menu principale pulsante", - "aria/Block User": "Blocca utente", - "aria/Bookmark Message": "Salva messaggio", - "aria/Cancel recording": "Annulla registrazione", - "aria/Cancel Reply": "Annulla risposta", - "aria/Channel Actions": "Azioni canale", - "aria/Channel details": "Dettagli canale", - "aria/Channel list": "Elenco dei canali", - "aria/Chat view controls": "Controlli visualizzazione chat", - "aria/Chat: {{ channelName }}": "Chat: {{ channelName }}", - "aria/Clear search": "Cancella ricerca", - "aria/Close callout dialog": "Chiudi finestra informativa", - "aria/Close thread": "Chiudi discussione", - "aria/Collapse sidebar": "Comprimi barra laterale", - "aria/Command activated: {{ command }}": "Comando attivato: {{ command }}", - "aria/Command Suggestions": "Suggerimenti comandi", - "aria/Complete recording": "Completa registrazione", - "aria/Copy Message Text": "Copia testo messaggio", - "aria/Decrease value": "Diminuisci valore", - "aria/Delete Message": "Elimina messaggio", - "aria/Delivered": "Consegnato", - "aria/Delivery status: {{ deliveryStatus }}": "Stato di consegna: {{ deliveryStatus }}", - "aria/Dismiss notification": "Chiudi notifica", - "aria/Download attachment": "Scarica l'allegato", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "«{{ option }}» rilasciato in posizione {{ position }}.", - "aria/Edit Message": "Modifica messaggio", - "aria/Emoji picker": "Selettore di emoji", - "aria/Emoji Suggestions": "Suggerimenti emoji", - "aria/Exit search": "Esci dalla ricerca", - "aria/Expand sidebar": "Espandi barra laterale", - "aria/file": "file", - "aria/File upload": "Caricamento di file", - "aria/Flag Message": "Segnala messaggio", - "aria/GIF": "GIF", - "aria/Giphy actions": "Azioni Giphy", - "aria/Giphy canceled": "Giphy annullato", - "aria/Giphy image changed": "Immagine Giphy cambiata", - "aria/Giphy image changed: {{ title }}": "Immagine Giphy cambiata: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Anteprima Giphy, visibile solo a te. Usa le azioni Invia, Mescola o Annulla.", - "aria/Giphy sent": "Giphy inviato", - "aria/Go back": "Indietro", - "aria/image": "immagine", - "aria/Image failed to load": "Caricamento immagine non riuscito", - "aria/Increase value": "Aumenta valore", - "aria/Jump to latest message": "Vai all'ultimo messaggio", - "aria/Jump to quoted message": "Vai al messaggio citato", - "aria/Last activity: {{ time }}": "Ultima attività: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "Ultimo messaggio da {{ sender }}: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "Ultimo messaggio: {{ messagePreview }}", - "aria/Mark Message Unread": "Contrassegna come non letto", - "aria/Mark messages as read": "Segna i messaggi come letti", - "aria/Mention Suggestions": "Suggerimenti di menzione", - "aria/Message Actions": "Azioni del messaggio", - "aria/Message from {{ user }},": "Messaggio di {{ user }},", - "aria/Message input": "Campo messaggio", - "aria/Message with attachments": "Messaggio con allegati", - "aria/Message,": "Messaggio,", - "aria/Mute User": "Mute utente", - "aria/Next page": "Pagina successiva", - "aria/No search results found": "Nessun risultato di ricerca trovato", - "aria/Notifications": "Notifiche", - "aria/Open Attachment Selector": "Apri selettore allegati", - "aria/Open Channel Actions Menu": "Apri menu azioni canale", - "aria/Open channel details": "Apri dettagli canale", - "aria/Open channels view": "Apri visualizzazione canali", - "aria/Open image shared by {{ name }}": "Apri l'immagine condivisa da {{ name }}", - "aria/Open Message Actions Menu": "Apri il menu delle azioni di messaggio", - "aria/Open Reaction Selector": "Apri il selettore di reazione", - "aria/Open Thread": "Apri discussione", - "aria/Open threads view": "Apri visualizzazione discussioni", - "aria/Open threads view with unread threads_one": "Apri visualizzazione discussioni, {{ count }} discussione non letta", - "aria/Open threads view with unread threads_many": "Apri visualizzazione discussioni, {{ count }} discussioni non lette", - "aria/Open threads view with unread threads_other": "Apri visualizzazione discussioni, {{ count }} discussioni non lette", - "aria/Open video shared by {{ name }}": "Apri il video condiviso da {{ name }}", - "aria/Opened channel: {{ name }}": "Canale aperto: {{ name }}", - "aria/Opened thread in {{ name }}": "Thread aperto in {{ name }}", - "aria/Option {{ position }}": "Opzione {{ position }}", - "aria/Options can now be reordered and removed.": "Ora le opzioni possono essere riordinate e rimosse.", - "aria/Pause": "Pausa", - "aria/Pause recording": "Metti in pausa registrazione", - "aria/Percent complete": "{{percent}} percento completato", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "Hai preso «{{ option }}». Usa le frecce per riordinare. Premi Spazio o Tab per rilasciare.", - "aria/Pin Message": "Appunta messaggio", - "aria/Play": "Riproduci", - "aria/Poll dialog opened": "Finestra di dialogo del sondaggio aperta", - "aria/Poll sent": "Sondaggio inviato", - "aria/Poll: {{ pollName }}": "Sondaggio: {{ pollName }}", - "aria/Press Enter to start typing": "Premi Invio per iniziare a digitare", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "Premi la barra spaziatrice per selezionare questa opzione, usa i tasti freccia Su e Giù per spostarla, quindi premi di nuovo la barra spaziatrice per deselezionarla.", - "aria/Previous page": "Pagina precedente", - "aria/Quote Message": "Citazione messaggio", - "aria/Reaction list": "Elenco delle reazioni", - "aria/Read": "Letto", - "aria/Recording paused": "Registrazione in pausa", - "aria/Recording resumed": "Registrazione ripresa", - "aria/Recording started": "Registrazione avviata", - "aria/Remind Me Message": "Ricordami", - "aria/Remove attachment": "Rimuovi allegato", - "aria/Remove location attachment": "Rimuovi allegato posizione", - "aria/Remove option: {{ option }}": "Rimuovi opzione: {{ option }}", - "aria/Remove Reminder": "Rimuovi promemoria", - "aria/Remove Save For Later": "Rimuovi Salva per dopo", - "aria/Removed option {{ option }}": "Opzione {{ option }} rimossa", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "Riordina «{{ option }}», posizione {{ position }} di {{ total }}", - "aria/Reorder option {{ position }}": "Riordina opzione {{ position }}", - "aria/Resend Message": "Invia di nuovo messaggio", - "aria/Resume recording": "Riprendi registrazione", - "aria/Retry upload": "Riprova caricamento", - "aria/Review bounced message": "Rivedi il messaggio respinto", - "aria/Search cleared": "Ricerca cancellata", - "aria/Search results": "Risultati della ricerca", - "aria/Search results header filter button": "Pulsante filtro intestazione risultati ricerca", - "aria/Search results header filter button for: {{ source }}": "Pulsante filtro intestazione risultati di ricerca per: {{ source }}", - "aria/Seek audio position": "Cerca posizione audio", - "aria/Select Reaction: {{ reactionName }}": "Seleziona reazione: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "Seleziona canale utente: {{ name }}", - "aria/Send": "Invia", - "aria/Sent": "Inviato", - "aria/Shared a link": "Link condiviso", - "aria/Shared a link with title: {{ linkTitle }}": "Link condiviso con titolo: {{ linkTitle }}", - "aria/Shared location": "Posizione condivisa", - "aria/Show preview": "Mostra anteprima", - "aria/Start recording audio": "Avvia registrazione audio", - "aria/Stop AI Generation": "Interrompi generazione IA", - "aria/Submenu": "Sottomenu", - "aria/Suggestions": "Suggerimenti", - "aria/There are no messages in this chat.": "Non ci sono messaggi in questa chat", - "aria/This option can be reordered and removed.": "Questa opzione può essere riordinata e rimossa.", - "aria/Thread list": "Elenco thread", - "aria/Thread: {{ messagePreview }}": "Discussione: {{ messagePreview }}", - "aria/Unblock User": "Sblocca utente", - "aria/Unmute User": "Riattiva il notifiche", - "aria/Unpin Message": "Rimuovi messaggio appuntato", - "aria/User selected: {{ user }}": "Utente selezionato: {{ user }}", - "aria/video": "video", - "aria/voice message": "messaggio vocale", - "aria/Voice message sent": "Messaggio vocale inviato", - "aria/Voice recording attached": "Registrazione vocale allegata", - "Ask a question": "Fai una domanda", - "Attach": "Allega", - "Attach files": "Allega file", - "Attachment": "Allegato", - "Attachment upload blocked due to {{reason}}": "Caricamento allegato bloccato a causa di {{reason}}", - "Attachment upload failed due to {{reason}}": "Caricamento allegato fallito a causa di {{reason}}", - "Back": "Indietro", - "ban-command-args": "[@nomeutente] [testo]", - "ban-command-description": "Vietare un utente", - "Block user": "Blocca utente", - "Block User": "Blocca utente", - "Browse channel members": "Sfoglia membri del canale", - "Browse pinned messages": "Sfoglia messaggi fissati", - "Cancel": "Annulla", - "Cannot seek in the recording": "Impossibile cercare nella registrazione", - "Changes saved": "Modifiche salvate", - "Channel archived": "Canale archiviato", - "Channel members": "Membri del canale", - "Channel Missing": "Il canale non esiste", - "Channel muted": "Canale silenziato", - "Channel pinned": "Canale fissato", - "Channel unarchived": "Archiviazione del canale annullata", - "Channel unmuted": "Canale non più silenziato", - "Channel unpinned": "Canale rimosso dai fissati", - "Channels": "Canali", - "Chat deleted": "Chat deleted", - "Chats": "Chat", - "Choose between 2 to 10 options": "Scegli tra 2 e 10 opzioni", - "Close": "Chiudi", - "Close dialog": "Chiudi finestra di dialogo", - "Close emoji picker": "Chiudi il selettore di emoji", - "Command not available while editing": "Comando non disponibile durante la modifica", - "Command not available while replying": "Comando non disponibile durante la risposta", - "Commands": "Comandi", - "Commands matching": "Comandi corrispondenti", - "Connection failure, reconnecting now...": "Errore di connessione, riconnessione in corso...", - "Contact info": "Informazioni contatto", - "Contact name": "Nome del contatto", - "Copy Message": "Copia messaggio", - "Create": "Crea", - "Create a question, add options, and configure poll settings": "Crea una domanda, aggiungi opzioni e configura le impostazioni del sondaggio", - "Create poll": "Crea sondaggio", - "Current location": "Posizione attuale", - "Delete": "Elimina", - "Delete chat": "Elimina chat", - "Delete for me": "Elimina per me", - "Delete message": "Elimina messaggio", - "Delivered": "Consegnato", - "Direct message": "Messaggio diretto", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "Vuoi terminare questo sondaggio ora? Nessuno potrà più votare in questo sondaggio.", - "Download {{ fileName }}": "Scarica {{ fileName }}", - "Download All": "Scarica tutto", - "Download Attachment": "Scarica allegato", - "Download attachment {{ name }}": "Scarica l'allegato {{ name }}", - "Download attachment {{ number }}": "Scarica l'allegato {{ number }}", - "Drag your files here": "Trascina i tuoi file qui", - "Drag your files here to add to your post": "Trascina i tuoi file qui per aggiungerli al tuo post", - "Due {{ timeLeft }}": "Scadenza tra {{ timeLeft }}", - "Due since {{ dueSince }}": "Scaduto dal {{ dueSince }}", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "Modifica", - "Edit chat data": "Modifica dati chat", - "Edit contact": "Modifica contatto", - "Edit group": "Modifica gruppo", - "Edit Message": "Modifica messaggio", - "Edit message request failed": "Richiesta di modifica del messaggio non riuscita", - "Edited": "Modificato", - "Emoji matching": "Abbinamento emoji", - "Empty message...": "Messaggio vuoto...", - "End": "Fine", - "End poll": "Termina sondaggio", - "End this poll?": "Terminare questo sondaggio?", - "End vote": "Termina il voto", - "Enforce unique vote is enabled": "Il voto unico è abilitato", - "Error": "Errore", - "Error · Unsent": "Errore · Non inviato", - "Error adding flag": "Errore durante l'aggiunta del flag", - "Error adding members": "Error adding members", - "Error blocking user": "Errore durante il blocco dell'utente", - "Error connecting to chat, refresh the page to try again.": "Errore di connessione alla chat, aggiorna la pagina per riprovare.", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "Errore durante l'eliminazione del messaggio", - "Error fetching reactions": "Errore nel caricamento delle reazioni", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "Errore durante la marcatura del messaggio come non letto. Impossibile marcare messaggi non letti più vecchi dei più recenti 100 messaggi del canale.", - "Error muting a user ...": "Errore nel silenziare un utente ...", - "Error muting channel": "Errore durante la disattivazione delle notifiche del canale", - "Error muting user": "Errore durante la disattivazione delle notifiche dell'utente", - "Error opening direct message": "Errore nell'apertura del messaggio diretto", - "Error pinning message": "Errore durante il blocco del messaggio", - "Error removing members": "Errore nella rimozione dei membri", - "Error removing message pin": "Errore durante la rimozione del PIN del messaggio", - "Error removing user": "Errore nella rimozione dell'utente", - "Error reproducing the recording": "Errore durante la riproduzione della registrazione", - "Error starting recording": "Errore durante l'avvio della registrazione", - "Error unblocking user": "Errore durante lo sblocco dell'utente", - "Error unmuting a user ...": "Errore nel riattivare un utente ...", - "Error unmuting channel": "Errore durante la riattivazione del canale", - "Error unmuting user": "Errore durante la riattivazione dell'utente", - "Error uploading attachment": "Errore durante il caricamento dell'allegato", - "Error uploading file": "Errore durante il caricamento del file", - "Error uploading image": "Errore durante il caricamento dell'immagine", - "Error: {{ errorMessage }}": "Errore: {{ errorMessage }}", - "Exit command {{ command }}": "Esci dal comando {{ command }}", - "Failed to block user": "Impossibile bloccare l'utente", - "Failed to create the poll": "Impossibile creare il sondaggio", - "Failed to create the poll due to {{reason}}": "Impossibile creare il sondaggio a causa di {{reason}}", - "Failed to delete the message": "Impossibile eliminare il messaggio", - "Failed to end the poll": "Impossibile terminare il sondaggio", - "Failed to end the poll due to {{reason}}": "Impossibile terminare il sondaggio a causa di {{reason}}", - "Failed to jump to the first unread message": "Impossibile passare al primo messaggio non letto", - "Failed to leave channel": "Impossibile lasciare il canale", - "Failed to load channels": "Impossibile caricare i canali", - "Failed to load more channels": "Impossibile caricare altri canali", - "Failed to mark channel as read": "Impossibile contrassegnare il canale come letto", - "Failed to play the recording": "Impossibile riprodurre la registrazione", - "Failed to retrieve location": "Impossibile recuperare la posizione", - "Failed to save changes": "Impossibile salvare le modifiche", - "Failed to share location": "Impossibile condividere la posizione", - "Failed to update channel archive status": "Impossibile aggiornare lo stato di archiviazione del canale", - "Failed to update channel mute status": "Impossibile aggiornare lo stato di silenziamento del canale", - "Failed to update channel pinned status": "Impossibile aggiornare lo stato di blocco del canale", - "File": "File", - "File is required for upload attachment": "È richiesto un file per caricare l'allegato", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "Il file è troppo grande: {{ size }}, la dimensione massima di caricamento è {{ limit }}", - "File too large": "File troppo grande", - "fileCount_one": "1 file", - "fileCount_many": "{{ count }} file", - "fileCount_other": "{{ count }} file", - "Files": "File", - "Flag": "Segnala", - "Generating...": "Generando...", - "giphy-command-args": "[testo]", - "giphy-command-description": "Pubblica un gif casuale sul canale", - "Go back": "Indietro", - "Group info": "Informazioni gruppo", - "Group name": "Nome del gruppo", - "Hide who voted": "Nascondi chi ha votato", - "Image": "Immagine", - "imageCount_one": "Immagine", - "imageCount_many": "{{ count }} immagini", - "imageCount_other": "{{ count }} immagini", - "Instant commands": "Comandi istantanei", - "language/af": "Afrikaans", - "language/am": "Amarico", - "language/ar": "Arabo", - "language/az": "Azero", - "language/bg": "Bulgaro", - "language/bn": "Bengalese", - "language/bs": "Bosniaco", - "language/cs": "Ceco", - "language/da": "Danese", - "language/de": "Tedesco", - "language/el": "Greco", - "language/en": "Inglese", - "language/es": "Spagnolo", - "language/es-MX": "Spagnolo (Messico)", - "language/et": "Estone", - "language/fa": "Persiano", - "language/fa-AF": "Dari", - "language/fi": "Finlandese", - "language/fr": "Francese", - "language/fr-CA": "Francese (Canada)", - "language/ha": "Hausa", - "language/he": "Ebraico", - "language/hi": "Hindi", - "language/hr": "Croato", - "language/ht": "Creolo haitiano", - "language/hu": "Ungherese", - "language/id": "Indonesiano", - "language/it": "Italiano", - "language/ja": "Giapponese", - "language/ka": "Georgiano", - "language/ko": "Coreano", - "language/lt": "Lituano", - "language/lv": "Lettone", - "language/ms": "Malese", - "language/nl": "Olandese", - "language/no": "Norvegese", - "language/pl": "Polacco", - "language/ps": "Pashto", - "language/pt": "Portoghese", - "language/ro": "Rumeno", - "language/ru": "Russo", - "language/sk": "Slovacco", - "language/sl": "Sloveno", - "language/so": "Somalo", - "language/sq": "Albanese", - "language/sr": "Serbo", - "language/sv": "Svedese", - "language/sw": "Swahili", - "language/ta": "Tamil", - "language/th": "Thai", - "language/tl": "Tagalog", - "language/tr": "Turco", - "language/uk": "Ucraino", - "language/ur": "Urdu", - "language/vi": "Vietnamita", - "language/zh": "Cinese (semplificato)", - "language/zh-TW": "Cinese (tradizionale)", - "Last seen {{ timestamp }}": "Ultimo accesso {{ timestamp }}", - "Leave Channel": "Lascia il canale", - "Leave chat": "Lascia il canale", - "Left channel": "Canale lasciato", - "Let others add options": "Lascia che altri aggiungano opzioni", - "Limit votes per person": "Limita i voti per persona", - "Link": "Collegamento", - "linkCount_one": "Link", - "linkCount_many": "{{ count }} link", - "linkCount_other": "{{ count }} link", - "live": "live", - "Live for {{duration}}": "Live per {{duration}}", - "Live location": "Posizione live", - "Live until {{ timestamp }}": "Live fino a {{ timestamp }}", - "Load more": "Carica di più", - "Local upload attachment missing local id": "Allegato di caricamento locale senza id locale", - "Location": "Posizione", - "Location sharing ended": "Condivisione posizione terminata", - "Location: {{ coordinates }}": "Posizione: {{ coordinates }}", - "Manage channel": "Gestisci canale", - "Manage members": "Gestisci membri", - "Mark as unread": "Contrassegna come non letto", - "Maximum number of votes (from 2 to 10)": "Numero massimo di voti (da 2 a 10)", - "Maximum votes per person": "Voti massimi per persona", - "Member detail": "Dettagli membro", - "mention/Channel": "Canale", - "mention/Channel Description": "Notifica tutti in questo canale", - "mention/Here": "Qui", - "mention/Here Description": "Notifica tutti i membri online in questo canale", - "Menu": "Menù", - "Message deleted": "Messaggio cancellato", - "Message Failed · Click to try again": "Invio messaggio fallito · Clicca per riprovare", - "Message Failed · Unauthorized": "Invio messaggio fallito · Non autorizzato", - "Message failed to send": "Invio del messaggio non riuscito", - "Message has been successfully flagged": "Il messaggio è stato segnalato con successo", - "Message marked as unread": "Messaggio contrassegnato come non letto", - "Message pinned": "Messaggio bloccato", - "Message unpinned": "Messaggio rimosso dai fissati", - "Message was blocked by moderation policies": "Il messaggio è stato bloccato dalle politiche di moderazione", - "Messages have been marked unread.": "I messaggi sono stati contrassegnati come non letti.", - "Missing permissions to upload the attachment": "Autorizzazioni mancanti per caricare l'allegato", - "Moderator": "Moderatore", - "Multiple votes": "Voti multipli", - "Mute": "Silenzia", - "Mute chat": "Disattiva notifiche chat", - "Mute user": "Disattiva notifiche utente", - "mute-command-args": "[@nomeutente]", - "mute-command-description": "Silenzia un utente", - "network error": "errore di rete", - "New": "Nuovo", - "New message from {{user}}": "Nuovo messaggio da {{user}}", - "New Messages!": "Nuovi messaggi!", - "Next": "Avanti", - "Next image": "Immagine successiva", - "No chats here yet…": "Non ci sono ancora messaggi qui...", - "No conversations yet": "Ancora nessuna conversazione", - "No files": "Nessun file", - "No items exist": "Nessun elemento presente", - "No member found": "Nessun membro trovato", - "No messages found": "Nessun messaggio trovato", - "No photos or videos": "Nessuna foto o video", - "No pinned messages": "Nessun messaggio fissato", - "No results found": "Nessun risultato trovato", - "No user found": "Nessun utente trovato", - "Nobody will be able to vote in this poll anymore.": "Nessuno potrà più votare in questo sondaggio.", - "Nothing yet...": "Ancora niente...", - "Notify all {{ role }} members": "Notifica tutti i membri con ruolo {{ role }}", - "Offline": "Offline", - "Ok": "OK", - "Online": "Online", - "Only numbers are allowed": "Sono consentiti solo numeri", - "Only visible to you": "Visibile solo per te", - "Open emoji picker": "Apri il selettore di emoji", - "Open gallery at image {{ index }}": "Apri la galleria all'immagine {{ index }}", - "Open image in gallery": "Apri immagine nella galleria", - "Open location in a map": "Apri posizione in una mappa", - "Open members actions": "Open members actions", - "Open menu": "Apri menu", - "Option already exists": "L'opzione esiste già", - "Option is empty": "L'opzione è vuota", - "Options": "Opzioni", - "Original": "Originale", - "Owner": "Proprietario", - "People matching": "Persone che corrispondono", - "Photo": "Foto", - "Photos & videos": "Foto e video", - "Pin": "Appunta", - "Pin a message to see it here": "Appunta un messaggio per vederlo qui", - "Pinned by {{ name }}": "Appuntato da {{ name }}", - "Pinned by You": "Fissato da te", - "Pinned message": "Messaggio fissato", - "Pinned messages": "Messaggi fissati", - "placeholder/PollComment": "Il tuo commento", - "placeholder/PollOptionSuggestion": "Inserisci una nuova opzione", - "Play video": "Riproduci video", - "Playback speed {{ rate }}x": "Velocità di riproduzione {{ rate }}x", - "Poll": "Sondaggio", - "Poll comments": "Commenti del sondaggio", - "Poll ended": "Sondaggio terminato", - "Poll options": "Opzioni del sondaggio", - "Poll results": "Risultati del sondaggio", - "Poll sent": "Sondaggio inviato", - "Previous": "Indietro", - "Previous image": "Immagine precedente", - "Question": "Domanda", - "Question {{ optionOrderNumber}}": "Domanda {{ optionOrderNumber}}", - "Question is required": "La domanda è obbligatoria", - "Quote Reply": "Rispondi con citazione", - "Reached the vote limit. Remove an existing vote first.": "Raggiunto il limite di voti. Rimuovi prima un voto esistente.", - "Recording format is not supported and cannot be reproduced": "Il formato di registrazione non è supportato e non può essere riprodotto", - "Remind me": "Promemoria", - "Remind Me": "Ricordami", - "Reminder set": "Promemoria impostato", - "Remove": "Rimuovi", - "Remove {{ count }} members_one": "Rimuovi {{ count }} membro", - "Remove {{ count }} members_many": "Rimuovi {{ count }} membri", - "Remove {{ count }} members_other": "Rimuovi {{ count }} membri", - "Remove {{ member }} from this channel?": "Rimuovere {{ member }} da questo canale?", - "Remove channel members": "Rimuovi membri del canale", - "Remove reminder": "Rimuovi promemoria", - "Remove save for later": "Rimuovi Salva per dopo", - "Remove user": "Rimuovi utente", - "Removed {{ count }} members_one": "Rimosso {{ count }} membro", - "Removed {{ count }} members_many": "Rimossi {{ count }} membri", - "Removed {{ count }} members_other": "Rimossi {{ count }} membri", - "Replied to a thread": "Ha risposto in un thread", - "Reply": "Rispondi", - "Reply to {{ authorName }}": "Rispondi a {{ authorName }}", - "Reply to a message to start a thread": "Rispondi a un messaggio per avviare un thread", - "Reply to Message": "Rispondi al messaggio", - "replyCount_one": "Una risposta", - "replyCount_many": "{{ count }} risposte", - "replyCount_other": "{{ count }} risposte", - "Resend": "Invia di nuovo", - "Retry upload": "Riprova caricamento", - "Review all options available in this poll": "Rivedi tutte le opzioni disponibili in questo sondaggio", - "Review comments submitted with poll answers": "Rivedi i commenti inviati con le risposte al sondaggio", - "Review poll results and open an option to see detailed votes": "Rivedi i risultati del sondaggio e apri un'opzione per vedere i voti dettagliati", - "Review this message and choose whether to delete it, edit it, or send it anyway": "Rivedi questo messaggio e scegli se eliminarlo, modificarlo o inviarlo comunque", - "Review who voted for this option": "Rivedi chi ha votato per questa opzione", - "Save": "Salva", - "Save for later": "Salva per dopo", - "Saved for later": "Salvato per dopo", - "Search": "Cerca", - "Search GIFs": "Cerca GIF", - "search-results-header-filter-source-button-label--channels": "canali", - "search-results-header-filter-source-button-label--messages": "messaggi", - "search-results-header-filter-source-button-label--users": "utenti", - "Searching for {{ searchSourceType }}...": "Ricerca di {{ searchSourceType }}...", - "Searching...": "Ricerca in corso...", - "searchResultsCount_one": "1 risultato", - "searchResultsCount_many": "{{ count }} risultati", - "searchResultsCount_other": "{{ count }} risultati", - "See all options ({{count}})_one": "Vedi tutte le opzioni ({{count}})", - "See all options ({{count}})_many": "Vedi tutte le opzioni ({{count}})", - "See all options ({{count}})_other": "Vedi tutte le opzioni ({{count}})", - "Select a thread to continue the conversation": "Seleziona un thread per continuare la conversazione", - "Select more than one option": "Seleziona più di un'opzione", - "Select one": "Seleziona uno", - "Select one or more": "Seleziona uno o più", - "Select up to {{count}}_one": "Seleziona fino a {{count}}", - "Select up to {{count}}_many": "Seleziona fino a {{count}}", - "Select up to {{count}}_other": "Seleziona fino a {{count}}", - "Select your current location and optionally enable live location sharing": "Seleziona la tua posizione attuale e abilita facoltativamente la condivisione della posizione in tempo reale", - "Send": "Invia", - "Send a message": "Invia un messaggio", - "Send a message to start the conversation": "Invia un messaggio per iniziare la conversazione", - "Send Anyway": "Invia comunque", - "Send direct message": "Invia messaggio diretto", - "Send message request failed": "Richiesta di invio messaggio non riuscita", - "Send poll": "Invia sondaggio", - "Sending...": "Invio in corso...", - "Sent": "Inviato", - "Share": "Condividi", - "Share a file to see it here": "Condividi un file per vederlo qui", - "Share a photo or video to see it here": "Condividi una foto o un video per vederlo qui", - "Share live location for": "Condividi posizione live per", - "Share Location": "Condividi posizione", - "Shared live location": "Posizione live condivisa", - "Shared location": "Posizione condivisa", - "Show all": "Mostra tutto", - "Shuffle": "Mescolare", - "size limit": "limite di dimensione", - "Slow Mode ON": "Modalità lenta attivata", - "Slow mode, wait {{ seconds }}s...": "Modalità lenta, attendi {{ seconds }} s...", - "Some of the files will not be accepted": "Alcuni dei file non saranno accettati", - "Start typing to search": "Inizia a digitare per cercare", - "Stop sharing": "Ferma condivisione", - "Submit": "Invia", - "Suggest a new option to add to this poll": "Suggerisci una nuova opzione da aggiungere a questo sondaggio", - "Suggest an option": "Suggerisci un'opzione", - "Tap to remove": "Tocca per rimuovere", - "Tap to remove: {{ reactionName }}": "Tocca per rimuovere: {{ reactionName }}", - "Thinking...": "Pensando...", - "this content could not be displayed": "questo contenuto non può essere mostrato", - "This field cannot be empty or contain only spaces": "Questo campo non può essere vuoto o contenere solo spazi", - "This message did not meet our content guidelines": "Questo messaggio non soddisfa le nostre linee guida sui contenuti", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "Questo utente potrà inviarti di nuovo messaggi.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "Discussione", - "Thread has not been found": "Discussione non trovata", - "Thread reply": "Risposta nella discussione", - "Thread Reply": "Risposta nella discussione", - "ThreadListUnseenThreadsBanner/loading": "Caricamento...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} thread non letto", - "ThreadListUnseenThreadsBanner/unreadThreads_many": "{{ count }} thread non letti", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} thread non letti", - "Threads": "Thread", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Ieri]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Oggi]\", \"nextDay\": \"[Domani]\", \"lastDay\": \"[Ieri]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Scorsa] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "{{ count }} g fa", - "timestamp/relativeToday": "Oggi", - "timestamp/relativeWeeksAgo": "{{ count }} sett fa", - "timestamp/relativeYesterday": "Ieri", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Oggi] [alle] HH:mm\", \"nextDay\": \"[Domani] [alle] HH:mm\", \"lastDay\": \"[Ieri] [alle] HH:mm\", \"nextWeek\": \"dddd [alle] HH:mm\", \"lastWeek\": \"[lo scorso] dddd [alle] HH:mm\", \"sameElse\": \"ddd, D MMM [alle] HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "Per iniziare a registrare, consenti l'accesso alla fotocamera nel tuo browser", - "To start recording, allow the microphone access in your browser": "Per iniziare a registrare, consenti l'accesso al microfono nel tuo browser", - "totalVoteCount_one": "1 voto in totale", - "totalVoteCount_many": "{{ count }} voti in totale", - "totalVoteCount_other": "{{ count }} voti in totale", - "Translated": "Tradotto", - "Translated from {{ language }}": "Tradotto da {{ language }}", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "Digita un numero da 2 a 10", - "Type your message": "Scrivi il tuo messaggio", - "Unarchive": "Ripristina", - "unban-command-args": "[@nomeutente]", - "unban-command-description": "Togliere il divieto a un utente", - "Unblock": "Sblocca", - "Unblock user": "Sblocca utente", - "Unblock User": "Sblocca utente", - "unknown error": "errore sconosciuto", - "Unmute": "Riattiva il notifiche", - "Unmute chat": "Riattiva chat", - "Unmute user": "Riattiva utente", - "unmute-command-args": "[@nomeutente]", - "unmute-command-description": "Togliere il silenzio a un utente", - "Unpin": "Sblocca", - "Unread messages": "Messaggi non letti", - "Unsupported attachment": "Allegato non supportato", - "unsupported file type": "tipo di file non supportato", - "Update": "Aggiorna", - "Update the comment attached to your poll answer": "Aggiorna il commento allegato alla tua risposta al sondaggio", - "Update your comment": "Aggiorna il tuo commento", - "Upload blocked": "Caricamento bloccato", - "Upload error": "Errore di caricamento", - "Upload failed": "Caricamento non riuscito", - "Upload Picture": "Carica immagine", - "Upload type: \"{{ type }}\" is not allowed": "Tipo di caricamento: \"{{ type }}\" non è consentito", - "User blocked": "Utente bloccato", - "User muted": "Utente silenziato", - "User removed": "Utente rimosso", - "User unblocked": "Utente sbloccato", - "User unmuted": "Utente riattivato", - "User uploaded content": "Contenuto caricato dall'utente", - "Video": "Video", - "videoCount_one": "Video", - "videoCount_many": "{{ count }} video", - "videoCount_other": "{{ count }} video", - "View": "Visualizza", - "View {{count}} comments_one": "Visualizza {{count}} commento", - "View {{count}} comments_many": "Visualizza {{count}} commenti", - "View {{count}} comments_other": "Visualizza {{count}} commenti", - "View all": "Visualizza tutto", - "View member details for {{ member }}": "Visualizza dettagli membro per {{ member }}", - "View original": "Visualizza originale", - "View results": "Vedi risultati", - "View translation": "Visualizza traduzione", - "Voice message": "Messaggio vocale", - "Voice message {{ duration }}": "Messaggio vocale {{ duration }}", - "Voice message deleted": "Messaggio vocale eliminato", - "voiceMessageCount_one": "Messaggio vocale", - "voiceMessageCount_many": "{{ count }} messaggi vocali", - "voiceMessageCount_other": "{{ count }} messaggi vocali", - "Vote ended": "Voto terminato", - "Votes": "Voti", - "Wait until all attachments have uploaded": "Attendi il caricamento di tutti gli allegati", - "Waiting for network…": "In attesa della rete…", - "You": "Tu", - "You have no channels currently": "Al momento non sono presenti canali", - "You've reached the maximum number of files": "Hai raggiunto il numero massimo di file" -} diff --git a/src/i18n/ja.json b/src/i18n/ja.json deleted file mode 100644 index 686d8b624a..0000000000 --- a/src/i18n/ja.json +++ /dev/null @@ -1,690 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} と {{ moreCount }} 他人", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} と {{ lastUser }}", - "{{ count }} files_one": "{{ count }} ファイル", - "{{ count }} files_other": "{{ count }} ファイル", - "{{ count }} members_other": "{{ count }}人のメンバー", - "{{ count }} members added_other": "{{ count }}人のメンバーを追加しました", - "{{ count }} people are typing_one": "{{ count }}人が入力中です", - "{{ count }} people are typing_many": "{{ count }}人が入力中です", - "{{ count }} people are typing_other": "{{ count }}人が入力中です", - "{{ count }} photos_one": "{{ count }} 写真", - "{{ count }} photos_other": "{{ count }} 写真", - "{{ count }} reactions_other": "{{ count }}件のリアクション", - "{{ count }} videos_one": "{{ count }} 動画", - "{{ count }} videos_other": "{{ count }} 動画", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} と {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }} イメージ", - "{{ member }} will be able to message you again.": "{{ member }}は再びメッセージを送信できるようになります。", - "{{ member }} won't be able to message you anymore.": "{{ member }}はメッセージを送信できなくなります。", - "{{ memberCount }} members": "{{ memberCount }} メンバー", - "{{ typing }} are typing": "{{ typing }}が入力中です", - "{{ typing }} is typing": "{{ typing }}が入力中です", - "{{ user }} has been muted": "{{ user }} 無音されています", - "{{ user }} has been unmuted": "{{ user }} 無音されていません", - "{{ user }} is typing...": "{{ user }} が入力中...", - "{{ users }} and {{ user }} are typing...": "{{ users }} と {{ user }} が入力中...", - "{{ users }} and more are typing...": "{{ users }} とその他が入力中...", - "{{ watcherCount }} online": "{{ watcherCount }} オンライン", - "{{count}} new messages_one": "{{count}}件の新しいメッセージ", - "{{count}} new messages_other": "{{count}}件の新しいメッセージ", - "{{count}} unread_one": "{{count}} 未読", - "{{count}} unread_other": "{{count}} 未読", - "{{count}} votes_one": "{{count}} 票", - "{{count}} votes_other": "{{count}} 票", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "あと{{count}}件のオプション", - "+{{count}} more options_other": "あと{{count}}件のオプション", - "🏙 Attachment...": "🏙 アタッチメント...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} が作成: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} が投票: {{pollOptionText}}", - "📍Shared location": "📍共有された位置情報", - "Actions": "Actions", - "Add": "追加", - "Add {{ count }} members_other": "{{ count }}人のメンバーを追加", - "Add a comment": "コメントを追加", - "Add a comment to your poll answer": "投票回答にコメントを追加", - "Add an option": "オプションを追加", - "Add channel members": "チャンネルメンバーを追加", - "Add members": "メンバーを追加", - "Add reaction": "リアクションを追加", - "Admin": "管理者", - "All results loaded": "すべての結果が読み込まれました", - "Allow access to camera": "カメラへのアクセスを許可する", - "Allow access to microphone": "マイクロフォンへのアクセスを許可する", - "Allow comments": "コメントを許可", - "Allow option suggestion": "オプションの提案を許可", - "Allow others to add comments": "他の人にコメントを追加することを許可する", - "Already a member": "すでにメンバーです", - "Also send as a direct message": "ダイレクトメッセージとしても送信", - "Also send in channel": "チャンネルにも送信", - "Also sent in channel": "チャンネルにも送信済み", - "An error has occurred during recording": "録音中にエラーが発生しました", - "An error has occurred during the recording processing": "録音処理中にエラーが発生しました", - "Anonymous": "匿名", - "Anonymous poll": "匿名投票", - "Archive": "アーカイブ", - "Are you sure you want to delete this message?": "このメッセージを削除してもよろしいですか?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_other": "添付ファイル {{ count }} 件", - "aria/{{ count }} search results_other": "{{ count }} 件の検索結果", - "aria/{{ count }} suggestions_other": "候補 {{ count }} 件", - "aria/{{ count }} unread message_other": "未読メッセージ {{ count }} 件", - "aria/{{ setting }} disabled": "{{ setting }} をオフにしました", - "aria/{{ setting }} enabled": "{{ setting }} をオンにしました", - "aria/Active": "アクティブ", - "aria/Animated GIF": "アニメーションGIF", - "aria/Animated GIF: {{ title }}": "アニメーションGIF:{{ title }}", - "aria/Attachment": "添付ファイル", - "aria/Attachment {{ attachmentType }}": "添付ファイル {{ attachmentType }}", - "aria/Attachment Actions": "添付ファイルの操作", - "aria/audio": "オーディオ", - "aria/Audio position {{ elapsed }} of {{ duration }}": "音声位置 {{ elapsed }} / {{ duration }}", - "aria/Audio position {{ progress }} percent": "音声位置 {{ progress }} パーセント", - "aria/Back to attachments": "添付ファイルに戻る", - "aria/Back to parent menu button": "親メニューに戻るボタン", - "aria/Block User": "ユーザーをブロック", - "aria/Bookmark Message": "メッセージをブックマーク", - "aria/Cancel recording": "録音をキャンセル", - "aria/Cancel Reply": "返信をキャンセル", - "aria/Channel Actions": "チャンネル操作", - "aria/Channel details": "チャンネル詳細", - "aria/Channel list": "チャンネル一覧", - "aria/Chat view controls": "チャットビューのコントロール", - "aria/Chat: {{ channelName }}": "チャット: {{ channelName }}", - "aria/Clear search": "検索をクリア", - "aria/Close callout dialog": "吹き出しダイアログを閉じる", - "aria/Close thread": "スレッドを閉じる", - "aria/Collapse sidebar": "サイドバーを折りたたむ", - "aria/Command activated: {{ command }}": "コマンドを有効化しました: {{ command }}", - "aria/Command Suggestions": "コマンド候補", - "aria/Complete recording": "録音を完了", - "aria/Copy Message Text": "メッセージテキストをコピー", - "aria/Decrease value": "値を減らす", - "aria/Delete Message": "メッセージを削除", - "aria/Delivered": "配信済み", - "aria/Delivery status: {{ deliveryStatus }}": "配信状況: {{ deliveryStatus }}", - "aria/Dismiss notification": "通知を閉じる", - "aria/Download attachment": "添付ファイルをダウンロード", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "「{{ option }}」を位置 {{ position }} に配置しました。", - "aria/Edit Message": "メッセージを編集", - "aria/Emoji picker": "絵文字ピッカー", - "aria/Emoji Suggestions": "絵文字候補", - "aria/Exit search": "検索を終了", - "aria/Expand sidebar": "サイドバーを展開", - "aria/file": "ファイル", - "aria/File upload": "ファイルアップロード", - "aria/Flag Message": "メッセージをフラグ", - "aria/GIF": "GIF", - "aria/Giphy actions": "Giphyの操作", - "aria/Giphy canceled": "Giphyをキャンセルしました", - "aria/Giphy image changed": "Giphy画像が変更されました", - "aria/Giphy image changed: {{ title }}": "Giphy画像が変更されました:{{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Giphyのプレビューです。あなたにのみ表示されます。送信、シャッフル、またはキャンセルの操作を使用してください。", - "aria/Giphy sent": "Giphyを送信しました", - "aria/Go back": "戻る", - "aria/image": "画像", - "aria/Image failed to load": "画像の読み込みに失敗しました", - "aria/Increase value": "値を増やす", - "aria/Jump to latest message": "最新のメッセージに移動", - "aria/Jump to quoted message": "引用されたメッセージに移動", - "aria/Last activity: {{ time }}": "最終アクティビティ: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "{{ sender }} からの最新メッセージ: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "最新メッセージ: {{ messagePreview }}", - "aria/Mark Message Unread": "未読としてマーク", - "aria/Mark messages as read": "メッセージを既読にする", - "aria/Mention Suggestions": "メンション候補", - "aria/Message Actions": "メッセージ操作", - "aria/Message from {{ user }},": "{{ user }}さんからのメッセージ,", - "aria/Message input": "メッセージ入力", - "aria/Message with attachments": "添付ファイル付きのメッセージ", - "aria/Message,": "メッセージ,", - "aria/Mute User": "ユーザーをミュート", - "aria/Next page": "次のページ", - "aria/No search results found": "検索結果が見つかりませんでした", - "aria/Notifications": "通知", - "aria/Open Attachment Selector": "添付ファイル選択を開く", - "aria/Open Channel Actions Menu": "チャンネルアクションメニューを開く", - "aria/Open channel details": "チャンネル詳細を開く", - "aria/Open channels view": "チャンネルビューを開く", - "aria/Open image shared by {{ name }}": "{{ name }}が共有した画像を開く", - "aria/Open Message Actions Menu": "メッセージアクションメニューを開く", - "aria/Open Reaction Selector": "リアクションセレクターを開く", - "aria/Open Thread": "スレッドを開く", - "aria/Open threads view": "スレッドビューを開く", - "aria/Open threads view with unread threads_one": "スレッドビューを開く、未読スレッド{{ count }}件", - "aria/Open threads view with unread threads_other": "スレッドビューを開く、未読スレッド{{ count }}件", - "aria/Open video shared by {{ name }}": "{{ name }}が共有した動画を開く", - "aria/Opened channel: {{ name }}": "チャンネルを開きました: {{ name }}", - "aria/Opened thread in {{ name }}": "{{ name }} でスレッドを開きました", - "aria/Option {{ position }}": "オプション {{ position }}", - "aria/Options can now be reordered and removed.": "選択肢の並べ替えと削除ができるようになりました。", - "aria/Pause": "一時停止", - "aria/Pause recording": "録音を一時停止", - "aria/Percent complete": "{{percent}}パーセント完了", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "「{{ option }}」を持ち上げました。矢印キーで並べ替え、スペースまたはタブで配置してください。", - "aria/Pin Message": "メッセージをピン", - "aria/Play": "再生", - "aria/Poll dialog opened": "投票ダイアログを開きました", - "aria/Poll sent": "投票を送信しました", - "aria/Poll: {{ pollName }}": "投票: {{ pollName }}", - "aria/Press Enter to start typing": "Enter キーを押して入力を開始します", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "このオプションを選択するにはスペースキーを押し、移動するには上下矢印キーを使用し、選択を解除するにはもう一度スペースキーを押してください。", - "aria/Previous page": "前のページ", - "aria/Quote Message": "メッセージを引用", - "aria/Reaction list": "リアクション一覧", - "aria/Read": "既読", - "aria/Recording paused": "録音を一時停止しました", - "aria/Recording resumed": "録音を再開しました", - "aria/Recording started": "録音を開始しました", - "aria/Remind Me Message": "リマインダー", - "aria/Remove attachment": "添付ファイルを削除", - "aria/Remove location attachment": "位置情報の添付ファイルを削除", - "aria/Remove option: {{ option }}": "オプションを削除: {{ option }}", - "aria/Remove Reminder": "リマインダーを削除", - "aria/Remove Save For Later": "「後で見る」を削除", - "aria/Removed option {{ option }}": "選択肢「{{ option }}」を削除しました", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "「{{ option }}」を並べ替え、{{ total }} 中 {{ position }} 番目", - "aria/Reorder option {{ position }}": "オプション {{ position }} を並べ替える", - "aria/Resend Message": "メッセージを再送信", - "aria/Resume recording": "録音を再開", - "aria/Retry upload": "アップロードを再試行", - "aria/Review bounced message": "バウンスされたメッセージを確認", - "aria/Search cleared": "検索をクリアしました", - "aria/Search results": "検索結果", - "aria/Search results header filter button": "検索結果ヘッダーフィルターボタン", - "aria/Search results header filter button for: {{ source }}": "{{ source }} の検索結果ヘッダーのフィルターボタン", - "aria/Seek audio position": "音声位置をシーク", - "aria/Select Reaction: {{ reactionName }}": "リアクションを選択: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "ユーザーチャンネルを選択: {{ name }}", - "aria/Send": "送信", - "aria/Sent": "送信済み", - "aria/Shared a link": "リンクを共有しました", - "aria/Shared a link with title: {{ linkTitle }}": "タイトル付きのリンクを共有しました: {{ linkTitle }}", - "aria/Shared location": "共有された位置情報", - "aria/Show preview": "プレビューを表示", - "aria/Start recording audio": "音声録音を開始", - "aria/Stop AI Generation": "AI生成を停止", - "aria/Submenu": "サブメニュー", - "aria/Suggestions": "候補", - "aria/There are no messages in this chat.": "このチャットにはメッセージがありません", - "aria/This option can be reordered and removed.": "この選択肢は並べ替えと削除ができます。", - "aria/Thread list": "スレッド一覧", - "aria/Thread: {{ messagePreview }}": "スレッド: {{ messagePreview }}", - "aria/Unblock User": "ユーザーのブロックを解除", - "aria/Unmute User": "無音を解除する", - "aria/Unpin Message": "ピンを解除", - "aria/User selected: {{ user }}": "選択したユーザー:{{ user }}", - "aria/video": "動画", - "aria/voice message": "ボイスメッセージ", - "aria/Voice message sent": "ボイスメッセージを送信しました", - "aria/Voice recording attached": "ボイス録音を添付しました", - "Ask a question": "質問する", - "Attach": "添付", - "Attach files": "ファイルを添付する", - "Attachment": "添付ファイル", - "Attachment upload blocked due to {{reason}}": "{{reason}}のため添付ファイルのアップロードがブロックされました", - "Attachment upload failed due to {{reason}}": "{{reason}}のため添付ファイルのアップロードに失敗しました", - "Back": "戻る", - "ban-command-args": "[@ユーザ名] [テキスト]", - "ban-command-description": "ユーザーを禁止する", - "Block user": "ユーザーをブロック", - "Block User": "ユーザーをブロック", - "Browse channel members": "チャンネルメンバーを表示", - "Browse pinned messages": "ピン留めメッセージを表示", - "Cancel": "キャンセル", - "Cannot seek in the recording": "録音中にシークできません", - "Changes saved": "変更を保存しました", - "Channel archived": "チャンネルをアーカイブしました", - "Channel members": "チャンネルメンバー", - "Channel Missing": "チャネルがありません", - "Channel muted": "チャンネルをミュートしました", - "Channel pinned": "チャンネルをピン留めしました", - "Channel unarchived": "チャンネルのアーカイブを解除しました", - "Channel unmuted": "チャンネルのミュートを解除しました", - "Channel unpinned": "チャンネルのピン留めを解除しました", - "Channels": "チャンネル", - "Chat deleted": "Chat deleted", - "Chats": "チャット", - "Choose between 2 to 10 options": "2〜10の選択肢から選ぶ", - "Close": "閉める", - "Close dialog": "ダイアログを閉じる", - "Close emoji picker": "絵文字ピッカーを閉める", - "Command not available while editing": "編集中はコマンドを使用できません", - "Command not available while replying": "返信中はコマンドを使用できません", - "Commands": "コマンド", - "Commands matching": "一致するコマンド", - "Connection failure, reconnecting now...": "接続が失敗しました。再接続中...", - "Contact info": "連絡先情報", - "Contact name": "連絡先名", - "Copy Message": "メッセージをコピー", - "Create": "作成", - "Create a question, add options, and configure poll settings": "質問を作成し、選択肢を追加して投票設定を構成", - "Create poll": "投票を作成", - "Current location": "現在の位置", - "Delete": "消去", - "Delete chat": "チャットを削除", - "Delete for me": "自分用に削除", - "Delete message": "メッセージを削除", - "Delivered": "配信しました", - "Direct message": "ダイレクトメッセージ", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "このアンケートを今終了しますか?終了すると、誰も投票できなくなります。", - "Download {{ fileName }}": "{{ fileName }} をダウンロード", - "Download All": "すべてダウンロード", - "Download Attachment": "添付ファイルをダウンロード", - "Download attachment {{ name }}": "添付ファイル {{ name }} をダウンロード", - "Download attachment {{ number }}": "添付ファイル {{ number }} をダウンロード", - "Drag your files here": "ここにファイルをドラッグ", - "Drag your files here to add to your post": "投稿に追加するためにここにファイルをドラッグ", - "Due {{ timeLeft }}": "{{ timeLeft }}に期限切れ", - "Due since {{ dueSince }}": "{{ dueSince }}から期限切れ", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "編集", - "Edit chat data": "チャットデータを編集", - "Edit contact": "連絡先を編集", - "Edit group": "グループを編集", - "Edit Message": "メッセージを編集", - "Edit message request failed": "メッセージの編集要求が失敗しました", - "Edited": "編集済み", - "Emoji matching": "絵文字マッチング", - "Empty message...": "空のメッセージ...", - "End": "終了", - "End poll": "アンケートを終了", - "End this poll?": "このアンケートを終了しますか?", - "End vote": "投票を終了", - "Enforce unique vote is enabled": "一意の投票が有効になっています", - "Error": "エラー", - "Error · Unsent": "エラー・未送信", - "Error adding flag": "フラグを追加のエラーが発生しました", - "Error adding members": "Error adding members", - "Error blocking user": "ユーザーのブロック中にエラーが発生しました", - "Error connecting to chat, refresh the page to try again.": "チャットへの接続ができませんでした。ページを更新してください。", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "メッセージを削除するエラーが発生しました", - "Error fetching reactions": "反応の読み込みエラー", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "メッセージを未読にする際にエラーが発生しました。最新の100件のチャンネルメッセージより古い未読メッセージはマークできません。", - "Error muting a user ...": "ユーザーを無音するエラーが発生しました...", - "Error muting channel": "チャンネルのミュート中にエラーが発生しました", - "Error muting user": "ユーザーのミュート中にエラーが発生しました", - "Error opening direct message": "ダイレクトメッセージを開く際にエラーが発生しました", - "Error pinning message": "メッセージをピンのエラーが発生しました", - "Error removing members": "メンバーの削除中にエラーが発生しました", - "Error removing message pin": "メッセージのピンを削除のエラーが発生しました", - "Error removing user": "ユーザーの削除中にエラーが発生しました", - "Error reproducing the recording": "録音の再生中にエラーが発生しました", - "Error starting recording": "録音の開始時にエラーが発生しました", - "Error unblocking user": "ユーザーのブロック解除中にエラーが発生しました", - "Error unmuting a user ...": "ユーザーの無音解除のエラーが発生しました...", - "Error unmuting channel": "チャンネルのミュート解除中にエラーが発生しました", - "Error unmuting user": "ユーザーのミュート解除中にエラーが発生しました", - "Error uploading attachment": "添付ファイルのアップロード中にエラーが発生しました", - "Error uploading file": "ファイルをアップロードのエラーが発生しました", - "Error uploading image": "画像をアップロードのエラーが発生しました", - "Error: {{ errorMessage }}": "エラー: {{ errorMessage }}", - "Exit command {{ command }}": "コマンドを終了 {{ command }}", - "Failed to block user": "ユーザーのブロックに失敗しました", - "Failed to create the poll": "投票の作成に失敗しました", - "Failed to create the poll due to {{reason}}": "{{reason}} のため投票の作成に失敗しました", - "Failed to delete the message": "メッセージの削除に失敗しました", - "Failed to end the poll": "アンケートの終了に失敗しました", - "Failed to end the poll due to {{reason}}": "{{reason}}のためアンケートの終了に失敗しました", - "Failed to jump to the first unread message": "最初の未読メッセージにジャンプできませんでした", - "Failed to leave channel": "チャンネルの退出に失敗しました", - "Failed to load channels": "チャンネルの読み込みに失敗しました", - "Failed to load more channels": "さらにチャンネルを読み込めませんでした", - "Failed to mark channel as read": "チャンネルを既読にすることができませんでした", - "Failed to play the recording": "録音の再生に失敗しました", - "Failed to retrieve location": "位置情報の取得に失敗しました", - "Failed to save changes": "変更を保存できませんでした", - "Failed to share location": "位置情報の共有に失敗しました", - "Failed to update channel archive status": "チャンネルのアーカイブ状態の更新に失敗しました", - "Failed to update channel mute status": "チャンネルのミュート状態の更新に失敗しました", - "Failed to update channel pinned status": "チャンネルのピン状態の更新に失敗しました", - "File": "ファイル", - "File is required for upload attachment": "添付ファイルのアップロードにはファイルが必要です", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "ファイルが大きすぎます:{{ size }}、最大アップロードサイズは{{ limit }}です", - "File too large": "ファイルが大きすぎます", - "fileCount_other": "{{ count }}件のファイル", - "Files": "ファイル", - "Flag": "フラグ", - "Generating...": "生成中...", - "giphy-command-args": "[テキスト]", - "giphy-command-description": "チャンネルにランダムなGIFを投稿する", - "Go back": "戻る", - "Group info": "グループ情報", - "Group name": "グループ名", - "Hide who voted": "誰が投票したかを非表示にする", - "Image": "画像", - "imageCount_other": "{{ count }}件の画像", - "Instant commands": "インスタントコマンド", - "language/af": "アフリカース語", - "language/am": "アムハラ語", - "language/ar": "アラビア語", - "language/az": "アゼルバイジャン語", - "language/bg": "ブルガリア語", - "language/bn": "ベンガル語", - "language/bs": "ボスニア語", - "language/cs": "チェコ語", - "language/da": "デンマーク語", - "language/de": "ドイツ語", - "language/el": "ギリシャ語", - "language/en": "英語", - "language/es": "スペイン語", - "language/es-MX": "スペイン語(メキシコ)", - "language/et": "エストニア語", - "language/fa": "ペルシア語", - "language/fa-AF": "ダリー語", - "language/fi": "フィンランド語", - "language/fr": "フランス語", - "language/fr-CA": "フランス語(カナダ)", - "language/ha": "ハウサ語", - "language/he": "ヘブライ語", - "language/hi": "ヒンディー語", - "language/hr": "クロアチア語", - "language/ht": "ハイチ・クレオール語", - "language/hu": "ハンガリー語", - "language/id": "インドネシア語", - "language/it": "イタリア語", - "language/ja": "日本語", - "language/ka": "グルジア語", - "language/ko": "韓国語", - "language/lt": "リトアニア語", - "language/lv": "ラトビア語", - "language/ms": "マレー語", - "language/nl": "オランダ語", - "language/no": "ノルウェー語", - "language/pl": "ポーランド語", - "language/ps": "パシュトー語", - "language/pt": "ポルトガル語", - "language/ro": "ルーマニア語", - "language/ru": "ロシア語", - "language/sk": "スロバキア語", - "language/sl": "スロベニア語", - "language/so": "ソマリ語", - "language/sq": "アルバニア語", - "language/sr": "セルビア語", - "language/sv": "スウェーデン語", - "language/sw": "スワヒリ語", - "language/ta": "タミル語", - "language/th": "タイ語", - "language/tl": "タガログ語", - "language/tr": "トルコ語", - "language/uk": "ウクライナ語", - "language/ur": "ウルドゥー語", - "language/vi": "ベトナム語", - "language/zh": "中国語(簡体字)", - "language/zh-TW": "中国語(繁体字)", - "Last seen {{ timestamp }}": "最終表示 {{ timestamp }}", - "Leave Channel": "チャンネルを退出", - "Leave chat": "チャンネルを退出", - "Left channel": "チャンネルを退出しました", - "Let others add options": "他の人が選択肢を追加できるようにする", - "Limit votes per person": "1人あたりの投票数を制限する", - "Link": "リンク", - "linkCount_other": "{{ count }}件のリンク", - "live": "ライブ", - "Live for {{duration}}": "{{duration}}間ライブ", - "Live location": "ライブ位置情報", - "Live until {{ timestamp }}": "{{ timestamp }}までライブ", - "Load more": "もっと読み込む", - "Local upload attachment missing local id": "ローカルアップロード添付にローカルIDがありません", - "Location": "位置情報", - "Location sharing ended": "位置情報の共有が終了しました", - "Location: {{ coordinates }}": "位置: {{ coordinates }}", - "Manage channel": "チャンネルを管理", - "Manage members": "メンバーを管理", - "Mark as unread": "未読としてマーク", - "Maximum number of votes (from 2 to 10)": "最大投票数(2から10まで)", - "Maximum votes per person": "1人あたりの最大投票数", - "Member detail": "メンバー詳細", - "mention/Channel": "チャンネル", - "mention/Channel Description": "このチャンネルの全員に通知", - "mention/Here": "ここ", - "mention/Here Description": "このチャンネルのオンライン中の全メンバーに通知", - "Menu": "メニュー", - "Message deleted": "メッセージが削除されました", - "Message Failed · Click to try again": "メッセージが失敗しました · クリックして再試行してください", - "Message Failed · Unauthorized": "メッセージが失敗しました · 許可されていません", - "Message failed to send": "メッセージの送信に失敗しました", - "Message has been successfully flagged": "メッセージに正常にフラグが付けられました", - "Message marked as unread": "メッセージを未読にしました", - "Message pinned": "メッセージにピンが付けられました", - "Message unpinned": "メッセージのピン留めを解除しました", - "Message was blocked by moderation policies": "メッセージはモデレーションポリシーによってブロックされました", - "Messages have been marked unread.": "メッセージは未読としてマークされました。", - "Missing permissions to upload the attachment": "添付ファイルをアップロードするための許可がありません", - "Moderator": "モデレーター", - "Multiple votes": "複数投票", - "Mute": "無音", - "Mute chat": "チャットをミュート", - "Mute user": "ユーザーをミュート", - "mute-command-args": "[@ユーザ名]", - "mute-command-description": "ユーザーをミュートする", - "network error": "ネットワークエラー", - "New": "新しい", - "New message from {{user}}": "{{user}}からの新しいメッセージ", - "New Messages!": "新しいメッセージ!", - "Next": "次へ", - "Next image": "次の画像", - "No chats here yet…": "ここにはまだチャットはありません…", - "No conversations yet": "まだ会話はありません", - "No files": "ファイルはありません", - "No items exist": "項目がありません", - "No member found": "メンバーが見つかりません", - "No messages found": "メッセージが見つかりません", - "No photos or videos": "写真や動画はありません", - "No pinned messages": "ピン留めメッセージはありません", - "No results found": "結果が見つかりません", - "No user found": "ユーザーが見つかりません", - "Nobody will be able to vote in this poll anymore.": "この投票では、誰も投票できなくなります。", - "Nothing yet...": "まだ何もありません...", - "Notify all {{ role }} members": "{{ role }} メンバー全員に通知", - "Offline": "オフライン", - "Ok": "OK", - "Online": "オンライン", - "Only numbers are allowed": "数字のみ許可されています", - "Only visible to you": "あなただけに表示", - "Open emoji picker": "絵文字ピッカーを開く", - "Open gallery at image {{ index }}": "画像 {{ index }} でギャラリーを開く", - "Open image in gallery": "画像をギャラリーで開く", - "Open location in a map": "地図で位置情報を開く", - "Open members actions": "Open members actions", - "Open menu": "メニューを開く", - "Option already exists": "オプションは既に存在します", - "Option is empty": "オプションが空です", - "Options": "オプション", - "Original": "原文", - "Owner": "オーナー", - "People matching": "一致する人", - "Photo": "写真", - "Photos & videos": "写真と動画", - "Pin": "ピン", - "Pin a message to see it here": "ここに表示するにはメッセージをピン留めしてください", - "Pinned by {{ name }}": "{{ name }}がピンしました", - "Pinned by You": "あなたがピン留めしました", - "Pinned message": "ピン留めメッセージ", - "Pinned messages": "ピン留めメッセージ", - "placeholder/PollComment": "コメント", - "placeholder/PollOptionSuggestion": "新しい選択肢を入力", - "Play video": "動画を再生", - "Playback speed {{ rate }}x": "再生速度 {{ rate }}x", - "Poll": "投票", - "Poll comments": "投票コメント", - "Poll ended": "アンケート終了", - "Poll options": "投票オプション", - "Poll results": "投票結果", - "Poll sent": "アンケートを送信しました", - "Previous": "前へ", - "Previous image": "前の画像", - "Question": "質問", - "Question {{ optionOrderNumber}}": "質問 {{ optionOrderNumber}}", - "Question is required": "質問は必須です", - "Quote Reply": "引用返信", - "Reached the vote limit. Remove an existing vote first.": "投票制限に達しました。既存の投票を先に削除してください。", - "Recording format is not supported and cannot be reproduced": "録音形式はサポートされておらず、再生できません", - "Remind me": "リマインド", - "Remind Me": "リマインダー", - "Reminder set": "リマインダーを設定しました", - "Remove": "削除", - "Remove {{ count }} members_other": "{{ count }}人のメンバーを削除", - "Remove {{ member }} from this channel?": "{{ member }}をこのチャンネルから削除しますか?", - "Remove channel members": "チャンネルメンバーを削除", - "Remove reminder": "リマインダーを削除", - "Remove save for later": "「後で見る」を削除", - "Remove user": "ユーザーを削除", - "Removed {{ count }} members_other": "{{ count }}人のメンバーを削除しました", - "Replied to a thread": "スレッドに返信しました", - "Reply": "返事", - "Reply to {{ authorName }}": "{{ authorName }} に返信", - "Reply to a message to start a thread": "メッセージに返信してスレッドを開始してください", - "Reply to Message": "メッセージに返信", - "replyCount_one": "1件の返信", - "replyCount_other": "{{ count }} 返信", - "Resend": "再送信", - "Retry upload": "アップロードを再試行", - "Review all options available in this poll": "この投票で利用可能なすべての選択肢を確認", - "Review comments submitted with poll answers": "投票回答とともに送信されたコメントを確認", - "Review poll results and open an option to see detailed votes": "投票結果を確認し、選択肢を開いて詳細な投票を表示", - "Review this message and choose whether to delete it, edit it, or send it anyway": "このメッセージを確認し、削除・編集・そのまま送信するかを選択", - "Review who voted for this option": "この選択肢に投票した人を確認", - "Save": "保存", - "Save for later": "後で保存", - "Saved for later": "後で保存済み", - "Search": "探す", - "Search GIFs": "GIFを検索", - "search-results-header-filter-source-button-label--channels": "チャンネル", - "search-results-header-filter-source-button-label--messages": "メッセージ", - "search-results-header-filter-source-button-label--users": "ユーザー", - "Searching for {{ searchSourceType }}...": "{{ searchSourceType }}を検索中...", - "Searching...": "検索中...", - "searchResultsCount_one": "1件の結果", - "searchResultsCount_other": "{{ count }}件の結果", - "See all options ({{count}})_one": "すべてのオプションを見る ({{count}})", - "See all options ({{count}})_other": "すべてのオプションを見る ({{count}})", - "Select a thread to continue the conversation": "会話を続けるにはスレッドを選択してください", - "Select more than one option": "複数の選択肢を選ぶ", - "Select one": "1つ選択", - "Select one or more": "1つ以上選択", - "Select up to {{count}}_one": "最大{{count}}まで選択", - "Select up to {{count}}_other": "最大{{count}}まで選択", - "Select your current location and optionally enable live location sharing": "現在地を選択し、必要に応じてライブ位置情報共有を有効化", - "Send": "送信", - "Send a message": "メッセージを送る", - "Send a message to start the conversation": "メッセージを送って会話を始めましょう", - "Send Anyway": "とにかく送信する", - "Send direct message": "ダイレクトメッセージを送信", - "Send message request failed": "メッセージ送信リクエストが失敗しました", - "Send poll": "アンケートを送信", - "Sending...": "送信中...", - "Sent": "送信済み", - "Share": "共有", - "Share a file to see it here": "ファイルを共有するとここに表示されます", - "Share a photo or video to see it here": "写真や動画を共有するとここに表示されます", - "Share live location for": "ライブ位置情報を共有", - "Share Location": "位置情報を共有", - "Shared live location": "共有されたライブ位置情報", - "Shared location": "共有された位置情報", - "Show all": "すべて表示", - "Shuffle": "シャッフル", - "size limit": "サイズ制限", - "Slow Mode ON": "スローモードオン", - "Slow mode, wait {{ seconds }}s...": "スローモード、{{ seconds }}秒お待ちください...", - "Some of the files will not be accepted": "一部のファイルは受け付けられません", - "Start typing to search": "検索するには入力を開始してください", - "Stop sharing": "共有を停止", - "Submit": "送信", - "Suggest a new option to add to this poll": "この投票に追加する新しい選択肢を提案", - "Suggest an option": "オプションを提案", - "Tap to remove": "タップして削除", - "Tap to remove: {{ reactionName }}": "タップして削除: {{ reactionName }}", - "Thinking...": "考え中...", - "this content could not be displayed": "このコンテンツは表示できませんでした", - "This field cannot be empty or contain only spaces": "このフィールドは空にすることはできません。また、空白文字のみを含むこともできません", - "This message did not meet our content guidelines": "このメッセージはコンテンツガイドラインに適合していません", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "このユーザーは再びあなたにメッセージを送信できるようになります。", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "スレッド", - "Thread has not been found": "スレッドが見つかりませんでした", - "Thread reply": "スレッドの返信", - "Thread Reply": "スレッドの返信", - "ThreadListUnseenThreadsBanner/loading": "読み込み中...", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }}件の未読スレッド", - "Threads": "スレッド", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[昨日]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[今日]\", \"nextDay\": \"[明日]\", \"lastDay\": \"[昨日]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[先週の] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "{{ count }}日前", - "timestamp/relativeToday": "今日", - "timestamp/relativeWeeksAgo": "{{ count }}週間前", - "timestamp/relativeYesterday": "昨日", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[今日] HH:mm\", \"nextDay\": \"[明日] HH:mm\", \"lastDay\": \"[昨日] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"dddd HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "録音を開始するには、ブラウザーでカメラへのアクセスを許可してください", - "To start recording, allow the microphone access in your browser": "録音を開始するには、ブラウザーでマイクロフォンへのアクセスを許可してください", - "totalVoteCount_other": "合計{{ count }}票", - "Translated": "翻訳済み", - "Translated from {{ language }}": "{{ language }}から翻訳", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "2から10までの数字を入力してください", - "Type your message": "メッセージを入力してください", - "Unarchive": "アーカイブ解除", - "unban-command-args": "[@ユーザ名]", - "unban-command-description": "ユーザーの禁止を解除する", - "Unblock": "ブロック解除", - "Unblock user": "ユーザーのブロックを解除", - "Unblock User": "ユーザーのブロックを解除", - "unknown error": "不明なエラー", - "Unmute": "無音を解除する", - "Unmute chat": "チャットのミュートを解除", - "Unmute user": "ユーザーのミュートを解除", - "unmute-command-args": "[@ユーザ名]", - "unmute-command-description": "ユーザーのミュートを解除する", - "Unpin": "ピンを解除する", - "Unread messages": "未読メッセージ", - "Unsupported attachment": "サポートされていない添付ファイル", - "unsupported file type": "サポートされていないファイル形式", - "Update": "更新", - "Update the comment attached to your poll answer": "投票回答に添付されたコメントを更新", - "Update your comment": "コメントを更新", - "Upload blocked": "アップロードがブロックされました", - "Upload error": "アップロードエラー", - "Upload failed": "アップロードに失敗しました", - "Upload Picture": "画像をアップロード", - "Upload type: \"{{ type }}\" is not allowed": "アップロードタイプ:\"{{ type }}\"は許可されていません", - "User blocked": "ユーザーをブロックしました", - "User muted": "ユーザーをミュートしました", - "User removed": "ユーザーを削除しました", - "User unblocked": "ユーザーのブロックを解除しました", - "User unmuted": "ユーザーのミュートを解除しました", - "User uploaded content": "ユーザーがアップロードしたコンテンツ", - "Video": "動画", - "videoCount_other": "{{ count }}件の動画", - "View": "表示", - "View {{count}} comments_one": "{{count}} コメントを表示", - "View {{count}} comments_other": "{{count}} コメントを表示", - "View all": "すべて表示", - "View member details for {{ member }}": "{{ member }}のメンバー詳細を表示", - "View original": "原文を表示", - "View results": "結果を表示", - "View translation": "翻訳を表示", - "Voice message": "ボイスメッセージ", - "Voice message {{ duration }}": "ボイスメッセージ {{ duration }}", - "Voice message deleted": "ボイスメッセージが削除されました", - "voiceMessageCount_other": "{{ count }}件のボイスメッセージ", - "Vote ended": "投票が終了しました", - "Votes": "投票", - "Wait until all attachments have uploaded": "すべての添付ファイルがアップロードされるまでお待ちください", - "Waiting for network…": "ネットワークを待機中…", - "You": "あなた", - "You have no channels currently": "現在チャンネルはありません", - "You've reached the maximum number of files": "ファイルの最大数に達しました" -} diff --git a/src/i18n/ko.json b/src/i18n/ko.json deleted file mode 100644 index d08917f5c1..0000000000 --- a/src/i18n/ko.json +++ /dev/null @@ -1,690 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} 그리고 {{ moreCount }}명 더", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} 그리고 {{ lastUser }}", - "{{ count }} files_one": "{{ count }}개 파일", - "{{ count }} files_other": "{{ count }}개 파일", - "{{ count }} members_other": "{{ count }}명 멤버", - "{{ count }} members added_other": "{{ count }}명 멤버가 추가됨", - "{{ count }} people are typing_one": "{{ count }}명이 입력 중입니다", - "{{ count }} people are typing_many": "{{ count }}명이 입력 중입니다", - "{{ count }} people are typing_other": "{{ count }}명이 입력 중입니다", - "{{ count }} photos_one": "{{ count }}개 사진", - "{{ count }} photos_other": "{{ count }}개 사진", - "{{ count }} reactions_other": "{{ count }}개 반응", - "{{ count }} videos_one": "{{ count }}개 동영상", - "{{ count }} videos_other": "{{ count }}개 동영상", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} 그리고 {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }}개 더", - "{{ member }} will be able to message you again.": "{{ member }}님이 다시 메시지를 보낼 수 있습니다.", - "{{ member }} won't be able to message you anymore.": "{{ member }}님은 더 이상 메시지를 보낼 수 없습니다.", - "{{ memberCount }} members": "{{ memberCount }}명", - "{{ typing }} are typing": "{{ typing }} 입력 중입니다", - "{{ typing }} is typing": "{{ typing }} 입력 중입니다", - "{{ user }} has been muted": "{{ user }} 음소거되었습니다", - "{{ user }} has been unmuted": "{{ user }} 음소거가 해제되었습니다", - "{{ user }} is typing...": "{{ user }}이(가) 입력 중입니다...", - "{{ users }} and {{ user }} are typing...": "{{ users }}와(과) {{ user }}이(가) 입력 중입니다...", - "{{ users }} and more are typing...": "{{ users }}와(과) 더 많은 사람들이 입력 중입니다...", - "{{ watcherCount }} online": "{{ watcherCount }} 온라인", - "{{count}} new messages_one": "{{count}}개의 새 메시지", - "{{count}} new messages_other": "{{count}}개의 새 메시지", - "{{count}} unread_one": "{{count}} 읽지 않음", - "{{count}} unread_other": "{{count}} 읽지 않음", - "{{count}} votes_one": "{{count}} 투표", - "{{count}} votes_other": "{{count}} 투표", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "옵션 {{count}}개 더", - "+{{count}} more options_other": "옵션 {{count}}개 더", - "🏙 Attachment...": "🏙 부착...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}}이(가) 생성함: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}}이(가) 투표함: {{pollOptionText}}", - "📍Shared location": "📍공유된 위치", - "Actions": "Actions", - "Add": "추가", - "Add {{ count }} members_other": "{{ count }}명 멤버 추가", - "Add a comment": "댓글 추가", - "Add a comment to your poll answer": "투표 답변에 댓글 추가", - "Add an option": "옵션 추가", - "Add channel members": "채널 멤버 추가", - "Add members": "멤버 추가", - "Add reaction": "반응 추가", - "Admin": "관리자", - "All results loaded": "모든 결과가 로드되었습니다", - "Allow access to camera": "카메라에 대한 액세스 허용", - "Allow access to microphone": "마이크로폰에 대한 액세스 허용", - "Allow comments": "댓글 허용", - "Allow option suggestion": "옵션 제안 허용", - "Allow others to add comments": "다른 사람이 댓글을 추가할 수 있도록 허용", - "Already a member": "이미 멤버입니다", - "Also send as a direct message": "다이렉트 메시지로도 보내기", - "Also send in channel": "채널에도 보내기", - "Also sent in channel": "채널에도 전송됨", - "An error has occurred during recording": "녹음 중 오류가 발생했습니다", - "An error has occurred during the recording processing": "녹음 처리 중 오류가 발생했습니다", - "Anonymous": "익명", - "Anonymous poll": "익명 투표", - "Archive": "아카이브", - "Are you sure you want to delete this message?": "이 메시지를 삭제하시겠습니까?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_other": "첨부 파일 {{ count }}개", - "aria/{{ count }} search results_other": "검색 결과 {{ count }}개", - "aria/{{ count }} suggestions_other": "제안 {{ count }}개", - "aria/{{ count }} unread message_other": "읽지 않은 메시지 {{ count }}개", - "aria/{{ setting }} disabled": "{{ setting }} 해제", - "aria/{{ setting }} enabled": "{{ setting }} 사용", - "aria/Active": "활성", - "aria/Animated GIF": "움직이는 GIF", - "aria/Animated GIF: {{ title }}": "움직이는 GIF: {{ title }}", - "aria/Attachment": "첨부 파일", - "aria/Attachment {{ attachmentType }}": "첨부 파일 {{ attachmentType }}", - "aria/Attachment Actions": "첨부 파일 작업", - "aria/audio": "오디오", - "aria/Audio position {{ elapsed }} of {{ duration }}": "오디오 위치 {{ elapsed }} / {{ duration }}", - "aria/Audio position {{ progress }} percent": "오디오 위치 {{ progress }}퍼센트", - "aria/Back to attachments": "첨부 파일로 돌아가기", - "aria/Back to parent menu button": "상위 메뉴로 돌아가기 버튼", - "aria/Block User": "사용자 차단", - "aria/Bookmark Message": "메시지 북마크", - "aria/Cancel recording": "녹음 취소", - "aria/Cancel Reply": "답장 취소", - "aria/Channel Actions": "채널 작업", - "aria/Channel details": "채널 세부 정보", - "aria/Channel list": "채널 목록", - "aria/Chat view controls": "채팅 보기 컨트롤", - "aria/Chat: {{ channelName }}": "채팅: {{ channelName }}", - "aria/Clear search": "검색 지우기", - "aria/Close callout dialog": "콜아웃 대화 상자 닫기", - "aria/Close thread": "스레드 닫기", - "aria/Collapse sidebar": "사이드바 접기", - "aria/Command activated: {{ command }}": "명령 활성화됨: {{ command }}", - "aria/Command Suggestions": "명령어 제안", - "aria/Complete recording": "녹음 완료", - "aria/Copy Message Text": "메시지 텍스트 복사", - "aria/Decrease value": "값 감소", - "aria/Delete Message": "메시지 삭제", - "aria/Delivered": "전달됨", - "aria/Delivery status: {{ deliveryStatus }}": "전송 상태: {{ deliveryStatus }}", - "aria/Dismiss notification": "알림 닫기", - "aria/Download attachment": "첨부 파일 다운로드", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "\"{{ option }}\"을(를) {{ position }}번 위치에 놓았습니다.", - "aria/Edit Message": "메시지 수정", - "aria/Emoji picker": "이모지 선택기", - "aria/Emoji Suggestions": "이모지 제안", - "aria/Exit search": "검색 종료", - "aria/Expand sidebar": "사이드바 확장", - "aria/file": "파일", - "aria/File upload": "파일 업로드", - "aria/Flag Message": "메시지 신고", - "aria/GIF": "GIF", - "aria/Giphy actions": "Giphy 작업", - "aria/Giphy canceled": "Giphy를 취소했습니다", - "aria/Giphy image changed": "Giphy 이미지가 변경되었습니다", - "aria/Giphy image changed: {{ title }}": "Giphy 이미지가 변경되었습니다: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Giphy 미리 보기이며 나에게만 표시됩니다. 보내기, 셔플 또는 취소 작업을 사용하세요.", - "aria/Giphy sent": "Giphy를 보냈습니다", - "aria/Go back": "뒤로 가기", - "aria/image": "이미지", - "aria/Image failed to load": "이미지를 불러오지 못했습니다", - "aria/Increase value": "값 증가", - "aria/Jump to latest message": "최신 메시지로 이동", - "aria/Jump to quoted message": "인용된 메시지로 이동", - "aria/Last activity: {{ time }}": "마지막 활동: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "{{ sender }}님의 마지막 메시지: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "마지막 메시지: {{ messagePreview }}", - "aria/Mark Message Unread": "읽지 않음으로 표시", - "aria/Mark messages as read": "메시지를 읽음으로 표시", - "aria/Mention Suggestions": "멘션 제안", - "aria/Message Actions": "메시지 작업", - "aria/Message from {{ user }},": "{{ user }}의 메시지,", - "aria/Message input": "메시지 입력", - "aria/Message with attachments": "첨부 파일이 있는 메시지", - "aria/Message,": "메시지,", - "aria/Mute User": "사용자 음소거", - "aria/Next page": "다음 페이지", - "aria/No search results found": "검색 결과가 없습니다", - "aria/Notifications": "알림", - "aria/Open Attachment Selector": "첨부 파일 선택기 열기", - "aria/Open Channel Actions Menu": "채널 작업 메뉴 열기", - "aria/Open channel details": "채널 세부 정보 열기", - "aria/Open channels view": "채널 보기 열기", - "aria/Open image shared by {{ name }}": "{{ name }}님이 공유한 이미지 열기", - "aria/Open Message Actions Menu": "메시지 액션 메뉴 열기", - "aria/Open Reaction Selector": "반응 선택기 열기", - "aria/Open Thread": "스레드 열기", - "aria/Open threads view": "스레드 보기 열기", - "aria/Open threads view with unread threads_one": "스레드 보기 열기, 읽지 않은 스레드 {{ count }}개", - "aria/Open threads view with unread threads_other": "스레드 보기 열기, 읽지 않은 스레드 {{ count }}개", - "aria/Open video shared by {{ name }}": "{{ name }}님이 공유한 동영상 열기", - "aria/Opened channel: {{ name }}": "채널 열림: {{ name }}", - "aria/Opened thread in {{ name }}": "{{ name }}에서 스레드 열림", - "aria/Option {{ position }}": "옵션 {{ position }}", - "aria/Options can now be reordered and removed.": "이제 옵션의 순서를 변경하고 제거할 수 있습니다.", - "aria/Pause": "일시정지", - "aria/Pause recording": "녹음 일시정지", - "aria/Percent complete": "{{percent}}퍼센트 완료", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "\"{{ option }}\"을(를) 집었습니다. 화살표 키로 순서를 변경하고, 스페이스 또는 탭 키로 놓으세요.", - "aria/Pin Message": "메시지 고정", - "aria/Play": "재생", - "aria/Poll dialog opened": "투표 대화 상자 열림", - "aria/Poll sent": "투표를 보냈습니다", - "aria/Poll: {{ pollName }}": "투표: {{ pollName }}", - "aria/Press Enter to start typing": "Enter 키를 눌러 입력을 시작하세요", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "이 옵션을 선택하려면 스페이스 키를 누르고, 이동하려면 위쪽 및 아래쪽 화살표 키를 사용한 다음, 선택을 해제하려면 스페이스 키를 다시 누르세요.", - "aria/Previous page": "이전 페이지", - "aria/Quote Message": "메시지 인용", - "aria/Reaction list": "반응 목록", - "aria/Read": "읽음", - "aria/Recording paused": "녹음 일시정지됨", - "aria/Recording resumed": "녹음 재개됨", - "aria/Recording started": "녹음 시작됨", - "aria/Remind Me Message": "알림 설정", - "aria/Remove attachment": "첨부 파일 제거", - "aria/Remove location attachment": "위치 첨부 파일 제거", - "aria/Remove option: {{ option }}": "옵션 제거: {{ option }}", - "aria/Remove Reminder": "알림 제거", - "aria/Remove Save For Later": "나중에 보기 제거", - "aria/Removed option {{ option }}": "옵션 {{ option }}이(가) 제거되었습니다", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "\"{{ option }}\" 순서 변경, {{ total }}개 중 {{ position }}번째", - "aria/Reorder option {{ position }}": "옵션 {{ position }} 순서 변경", - "aria/Resend Message": "메시지 다시 보내기", - "aria/Resume recording": "녹음 재개", - "aria/Retry upload": "업로드 다시 시도", - "aria/Review bounced message": "반송된 메시지 검토", - "aria/Search cleared": "검색을 지웠습니다", - "aria/Search results": "검색 결과", - "aria/Search results header filter button": "검색 결과 헤더 필터 버튼", - "aria/Search results header filter button for: {{ source }}": "{{ source }}에 대한 검색 결과 헤더 필터 버튼", - "aria/Seek audio position": "오디오 위치 탐색", - "aria/Select Reaction: {{ reactionName }}": "반응 선택: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "사용자 채널 선택: {{ name }}", - "aria/Send": "보내기", - "aria/Sent": "보냄", - "aria/Shared a link": "링크를 공유했습니다", - "aria/Shared a link with title: {{ linkTitle }}": "제목과 함께 링크를 공유했습니다: {{ linkTitle }}", - "aria/Shared location": "공유된 위치", - "aria/Show preview": "미리보기 표시", - "aria/Start recording audio": "오디오 녹음 시작", - "aria/Stop AI Generation": "AI 생성 중지", - "aria/Submenu": "하위 메뉴", - "aria/Suggestions": "제안", - "aria/There are no messages in this chat.": "이 채팅에 메시지가 없습니다", - "aria/This option can be reordered and removed.": "이 옵션은 순서를 변경하고 제거할 수 있습니다.", - "aria/Thread list": "스레드 목록", - "aria/Thread: {{ messagePreview }}": "스레드: {{ messagePreview }}", - "aria/Unblock User": "사용자 차단 해제", - "aria/Unmute User": "음소거 해제", - "aria/Unpin Message": "핀 해제", - "aria/User selected: {{ user }}": "선택한 사용자: {{ user }}", - "aria/video": "동영상", - "aria/voice message": "음성 메시지", - "aria/Voice message sent": "음성 메시지 전송됨", - "aria/Voice recording attached": "음성 녹음 첨부됨", - "Ask a question": "질문하기", - "Attach": "첨부", - "Attach files": "파일 첨부", - "Attachment": "첨부 파일", - "Attachment upload blocked due to {{reason}}": "{{reason}}로 인해 첨부 파일 업로드가 차단되었습니다", - "Attachment upload failed due to {{reason}}": "{{reason}}로 인해 첨부 파일 업로드가 실패했습니다", - "Back": "뒤로", - "ban-command-args": "[@사용자이름] [텍스트]", - "ban-command-description": "사용자를 차단", - "Block user": "사용자 차단", - "Block User": "사용자 차단", - "Browse channel members": "채널 멤버 보기", - "Browse pinned messages": "고정된 메시지 보기", - "Cancel": "취소", - "Cannot seek in the recording": "녹음에서 찾을 수 없습니다", - "Changes saved": "변경 사항이 저장되었습니다", - "Channel archived": "채널이 보관됨", - "Channel members": "채널 멤버", - "Channel Missing": "채널 누락", - "Channel muted": "채널이 음소거됨", - "Channel pinned": "채널이 고정됨", - "Channel unarchived": "채널 보관이 해제됨", - "Channel unmuted": "채널 음소거가 해제됨", - "Channel unpinned": "채널 고정이 해제됨", - "Channels": "채널", - "Chat deleted": "Chat deleted", - "Chats": "채팅", - "Choose between 2 to 10 options": "2~10개의 선택지 중에서 선택", - "Close": "닫기", - "Close dialog": "대화 상자 닫기", - "Close emoji picker": "이모티콘 선택기 닫기", - "Command not available while editing": "편집 중에는 명령을 사용할 수 없습니다", - "Command not available while replying": "답장 중에는 명령을 사용할 수 없습니다", - "Commands": "명령어", - "Commands matching": "일치하는 명령", - "Connection failure, reconnecting now...": "연결 실패, 지금 다시 연결 중...", - "Contact info": "연락처 정보", - "Contact name": "연락처 이름", - "Copy Message": "메시지 복사", - "Create": "생성", - "Create a question, add options, and configure poll settings": "질문을 만들고 옵션을 추가한 뒤 투표 설정 구성", - "Create poll": "투표 생성", - "Current location": "현재 위치", - "Delete": "삭제", - "Delete chat": "채팅 삭제", - "Delete for me": "나만 삭제", - "Delete message": "메시지 삭제", - "Delivered": "배달됨", - "Direct message": "다이렉트 메시지", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "지금 이 투표를 종료하시겠습니까? 종료 후에는 더 이상 투표할 수 없습니다.", - "Download {{ fileName }}": "{{ fileName }} 다운로드", - "Download All": "모두 다운로드", - "Download Attachment": "첨부 파일 다운로드", - "Download attachment {{ name }}": "첨부 파일 {{ name }} 다운로드", - "Download attachment {{ number }}": "첨부 파일 {{ number }} 다운로드", - "Drag your files here": "여기로 파일을 끌어다 놓으세요", - "Drag your files here to add to your post": "게시물에 추가하려면 파일을 여기로 끌어다 놓으세요", - "Due {{ timeLeft }}": "{{ timeLeft }}에 기한", - "Due since {{ dueSince }}": "{{ dueSince }}부터 기한", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "수정", - "Edit chat data": "채팅 데이터 수정", - "Edit contact": "연락처 편집", - "Edit group": "그룹 편집", - "Edit Message": "메시지 수정", - "Edit message request failed": "메시지 수정 요청 실패", - "Edited": "편집됨", - "Emoji matching": "이모티콘 매칭", - "Empty message...": "빈 메시지...", - "End": "종료", - "End poll": "투표 종료", - "End this poll?": "이 투표를 종료하시겠습니까?", - "End vote": "투표 종료", - "Enforce unique vote is enabled": "고유 투표가 활성화되었습니다", - "Error": "오류", - "Error · Unsent": "오류 · 전송되지 않음", - "Error adding flag": "플래그를 추가하는 동안 오류가 발생했습니다.", - "Error adding members": "Error adding members", - "Error blocking user": "사용자 차단 중 오류 발생", - "Error connecting to chat, refresh the page to try again.": "채팅에 연결하는 동안 오류가 발생했습니다. 페이지를 새로고침하여 다시 시도하세요.", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "메시지를 삭제하는 중에 오류가 발생했습니다.", - "Error fetching reactions": "반응 로딩 오류.", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "메시지를 읽지 않음으로 표시하는 중 오류가 발생했습니다. 가장 최근 100개의 채널 메시지보다 오래된 읽지 않은 메시지는 표시할 수 없습니다.", - "Error muting a user ...": "사용자를 음소거하는 중에 오류가 발생했습니다...", - "Error muting channel": "채널 음소거 중 오류 발생", - "Error muting user": "사용자 음소거 중 오류 발생", - "Error opening direct message": "다이렉트 메시지를 여는 중 오류가 발생했습니다", - "Error pinning message": "메시지를 핀하는 중에 오류가 발생했습니다.", - "Error removing members": "멤버를 제거하는 중 오류가 발생했습니다", - "Error removing message pin": "메시지 핀을 제거하는 중에 오류가 발생했습니다.", - "Error removing user": "사용자를 제거하는 중 오류가 발생했습니다", - "Error reproducing the recording": "녹음 재생 중 오류 발생", - "Error starting recording": "녹음 시작 중 오류가 발생했습니다", - "Error unblocking user": "사용자 차단 해제 중 오류가 발생했습니다", - "Error unmuting a user ...": "사용자 음소거 해제 중 오류 발생...", - "Error unmuting channel": "채널 음소거 해제 중 오류 발생", - "Error unmuting user": "사용자 음소거 해제 중 오류 발생", - "Error uploading attachment": "첨부 파일 업로드 중 오류가 발생했습니다", - "Error uploading file": "파일 업로드 오류", - "Error uploading image": "이미지를 업로드하는 동안 오류가 발생했습니다.", - "Error: {{ errorMessage }}": "오류: {{ errorMessage }}", - "Exit command {{ command }}": "명령 종료 {{ command }}", - "Failed to block user": "사용자 차단에 실패했습니다", - "Failed to create the poll": "투표 생성 실패", - "Failed to create the poll due to {{reason}}": "{{reason}} 때문에 투표를 생성하지 못했습니다", - "Failed to delete the message": "메시지 삭제에 실패했습니다", - "Failed to end the poll": "투표 종료에 실패했습니다", - "Failed to end the poll due to {{reason}}": "{{reason}}(으)로 인해 투표 종료에 실패했습니다", - "Failed to jump to the first unread message": "첫 번째 읽지 않은 메시지로 이동하지 못했습니다", - "Failed to leave channel": "채널 나가기에 실패했습니다", - "Failed to load channels": "채널을 불러오지 못했습니다", - "Failed to load more channels": "채널을 더 불러오지 못했습니다", - "Failed to mark channel as read": "채널을 읽음으로 표시하는 데 실패했습니다", - "Failed to play the recording": "녹음을 재생하지 못했습니다", - "Failed to retrieve location": "위치를 가져오지 못했습니다", - "Failed to save changes": "변경 사항을 저장하지 못했습니다", - "Failed to share location": "위치를 공유하지 못했습니다", - "Failed to update channel archive status": "채널 아카이브 상태 업데이트에 실패했습니다", - "Failed to update channel mute status": "채널 음소거 상태 업데이트에 실패했습니다", - "Failed to update channel pinned status": "채널 고정 상태 업데이트에 실패했습니다", - "File": "파일", - "File is required for upload attachment": "첨부 파일 업로드에 파일이 필요합니다", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "파일이 너무 큽니다: {{ size }}, 최대 업로드 크기는 {{ limit }}입니다", - "File too large": "파일이 너무 큽니다", - "fileCount_other": "파일 {{ count }}개", - "Files": "파일", - "Flag": "플래그", - "Generating...": "생성 중...", - "giphy-command-args": "[텍스트]", - "giphy-command-description": "채널에 무작위 GIF 게시", - "Go back": "뒤로 가기", - "Group info": "그룹 정보", - "Group name": "그룹 이름", - "Hide who voted": "누가 투표했는지 숨기기", - "Image": "이미지", - "imageCount_other": "이미지 {{ count }}개", - "Instant commands": "즉시 명령어", - "language/af": "아프리칸스어", - "language/am": "암하라어", - "language/ar": "아랍어", - "language/az": "아제르바이잔어", - "language/bg": "불가리아어", - "language/bn": "벵골어", - "language/bs": "보스니아어", - "language/cs": "체코어", - "language/da": "덴마크어", - "language/de": "독일어", - "language/el": "그리스어", - "language/en": "영어", - "language/es": "스페인어", - "language/es-MX": "스페인어(멕시코)", - "language/et": "에스토니아어", - "language/fa": "페르시아어", - "language/fa-AF": "다리어", - "language/fi": "핀란드어", - "language/fr": "프랑스어", - "language/fr-CA": "프랑스어(캐나다)", - "language/ha": "하우사어", - "language/he": "히브리어", - "language/hi": "힌디어", - "language/hr": "크로아티아어", - "language/ht": "아이티 크리올어", - "language/hu": "헝가리어", - "language/id": "인도네시아어", - "language/it": "이탈리아어", - "language/ja": "일본어", - "language/ka": "조지아어", - "language/ko": "한국어", - "language/lt": "리투아니아어", - "language/lv": "라트비아어", - "language/ms": "말레이어", - "language/nl": "네덜란드어", - "language/no": "노르웨이어", - "language/pl": "폴란드어", - "language/ps": "파슈토어", - "language/pt": "포르투갈어", - "language/ro": "루마니아어", - "language/ru": "러시아어", - "language/sk": "슬로바키아어", - "language/sl": "슬로베니아어", - "language/so": "소말리아어", - "language/sq": "알바니아어", - "language/sr": "세르비아어", - "language/sv": "스웨덴어", - "language/sw": "스와힐리어", - "language/ta": "타밀어", - "language/th": "태국어", - "language/tl": "타갈로그어", - "language/tr": "터키어", - "language/uk": "우크라이나어", - "language/ur": "우르두어", - "language/vi": "베트남어", - "language/zh": "중국어(간체)", - "language/zh-TW": "중국어(번체)", - "Last seen {{ timestamp }}": "마지막 접속 {{ timestamp }}", - "Leave Channel": "채널 나가기", - "Leave chat": "채널 나가기", - "Left channel": "채널을 나갔습니다", - "Let others add options": "다른 사람이 선택지를 추가할 수 있도록 허용", - "Limit votes per person": "1인당 투표 수 제한", - "Link": "링크", - "linkCount_other": "링크 {{ count }}개", - "live": "라이브", - "Live for {{duration}}": "{{duration}} 동안 라이브", - "Live location": "라이브 위치", - "Live until {{ timestamp }}": "{{ timestamp }}까지 라이브", - "Load more": "더 불러오기", - "Local upload attachment missing local id": "로컬 업로드 첨부에 로컬 ID가 없습니다", - "Location": "위치", - "Location sharing ended": "위치 공유가 종료되었습니다", - "Location: {{ coordinates }}": "위치: {{ coordinates }}", - "Manage channel": "채널 관리", - "Manage members": "멤버 관리", - "Mark as unread": "읽지 않음으로 표시", - "Maximum number of votes (from 2 to 10)": "최대 투표 수 (2에서 10까지)", - "Maximum votes per person": "1인당 최대 투표 수", - "Member detail": "멤버 상세 정보", - "mention/Channel": "채널", - "mention/Channel Description": "이 채널의 모두에게 알림", - "mention/Here": "여기", - "mention/Here Description": "이 채널의 모든 온라인 멤버에게 알림", - "Menu": "메뉴", - "Message deleted": "메시지가 삭제되었습니다.", - "Message Failed · Click to try again": "메시지 실패 · 다시 시도하려면 클릭하세요.", - "Message Failed · Unauthorized": "메시지 실패 · 승인되지 않음", - "Message failed to send": "메시지 전송 실패", - "Message has been successfully flagged": "메시지에 플래그가 지정되었습니다.", - "Message marked as unread": "메시지를 읽지 않음으로 표시했습니다", - "Message pinned": "메시지 핀했습니다", - "Message unpinned": "메시지 고정이 해제됨", - "Message was blocked by moderation policies": "메시지가 관리 정책에 의해 차단되었습니다.", - "Messages have been marked unread.": "메시지가 읽지 않음으로 표시되었습니다.", - "Missing permissions to upload the attachment": "첨부 파일을 업로드하려면 권한이 필요합니다", - "Moderator": "운영자", - "Multiple votes": "복수 투표", - "Mute": "무음", - "Mute chat": "채팅 음소거", - "Mute user": "사용자 음소거", - "mute-command-args": "[@사용자이름]", - "mute-command-description": "사용자 음소거", - "network error": "네트워크 오류", - "New": "새로운", - "New message from {{user}}": "{{user}}의 새 메시지", - "New Messages!": "새 메시지!", - "Next": "다음", - "Next image": "다음 이미지", - "No chats here yet…": "아직 채팅이 없습니다...", - "No conversations yet": "아직 대화가 없습니다.", - "No files": "파일이 없습니다", - "No items exist": "항목이 없습니다.", - "No member found": "멤버를 찾을 수 없습니다", - "No messages found": "메시지를 찾을 수 없습니다", - "No photos or videos": "사진 또는 동영상이 없습니다", - "No pinned messages": "고정된 메시지가 없습니다", - "No results found": "검색 결과가 없습니다", - "No user found": "사용자를 찾을 수 없습니다", - "Nobody will be able to vote in this poll anymore.": "이 투표에 더 이상 아무도 투표할 수 없습니다.", - "Nothing yet...": "아직 아무것도...", - "Notify all {{ role }} members": "{{ role }} 역할의 모든 멤버에게 알림", - "Offline": "오프라인", - "Ok": "확인", - "Online": "온라인", - "Only numbers are allowed": "숫자만 입력 가능합니다", - "Only visible to you": "당신에게만 표시됨", - "Open emoji picker": "이모지 선택기 열기", - "Open gallery at image {{ index }}": "이미지 {{ index }}에서 갤러리 열기", - "Open image in gallery": "갤러리에서 이미지 열기", - "Open location in a map": "지도에서 위치 열기", - "Open members actions": "Open members actions", - "Open menu": "메뉴 열기", - "Option already exists": "옵션이 이미 존재합니다", - "Option is empty": "옵션이 비어 있습니다", - "Options": "옵션", - "Original": "원문", - "Owner": "소유자", - "People matching": "일치하는 사람", - "Photo": "사진", - "Photos & videos": "사진 및 동영상", - "Pin": "핀", - "Pin a message to see it here": "여기에서 보려면 메시지를 고정하세요", - "Pinned by {{ name }}": "{{ name }}님이 핀함", - "Pinned by You": "내가 고정함", - "Pinned message": "고정된 메시지", - "Pinned messages": "고정된 메시지", - "placeholder/PollComment": "댓글", - "placeholder/PollOptionSuggestion": "새 옵션 입력", - "Play video": "동영상 재생", - "Playback speed {{ rate }}x": "재생 속도 {{ rate }}x", - "Poll": "투표", - "Poll comments": "투표 댓글", - "Poll ended": "투표 종료됨", - "Poll options": "투표 옵션", - "Poll results": "투표 결과", - "Poll sent": "투표 전송됨", - "Previous": "이전", - "Previous image": "이전 이미지", - "Question": "질문", - "Question {{ optionOrderNumber}}": "질문 {{ optionOrderNumber}}", - "Question is required": "질문이 필요합니다", - "Quote Reply": "인용 답장", - "Reached the vote limit. Remove an existing vote first.": "투표 한도에 도달했습니다. 기존 투표를 먼저 제거하세요.", - "Recording format is not supported and cannot be reproduced": "녹음 형식이 지원되지 않으므로 재생할 수 없습니다", - "Remind me": "알림", - "Remind Me": "알림 설정", - "Reminder set": "알림 설정됨", - "Remove": "제거", - "Remove {{ count }} members_other": "{{ count }}명의 멤버 제거", - "Remove {{ member }} from this channel?": "이 채널에서 {{ member }}님을 제거하시겠습니까?", - "Remove channel members": "채널 멤버 제거", - "Remove reminder": "알림 제거", - "Remove save for later": "나중에 보기 제거", - "Remove user": "사용자 제거", - "Removed {{ count }} members_other": "{{ count }}명의 멤버를 제거했습니다", - "Replied to a thread": "스레드에 답글을 남겼습니다", - "Reply": "답장", - "Reply to {{ authorName }}": "{{ authorName }}님에게 답장", - "Reply to a message to start a thread": "스레드를 시작하려면 메시지에 답장하세요", - "Reply to Message": "메시지에 답장", - "replyCount_one": "답장 1개", - "replyCount_other": "{{ count }} 답장", - "Resend": "다시 보내기", - "Retry upload": "업로드 다시 시도", - "Review all options available in this poll": "이 투표에서 사용 가능한 모든 옵션 검토", - "Review comments submitted with poll answers": "투표 답변과 함께 제출된 댓글 검토", - "Review poll results and open an option to see detailed votes": "투표 결과를 검토하고 옵션을 열어 상세 득표 보기", - "Review this message and choose whether to delete it, edit it, or send it anyway": "이 메시지를 검토하고 삭제, 수정 또는 그대로 전송할지 선택", - "Review who voted for this option": "이 옵션에 투표한 사람 검토", - "Save": "저장", - "Save for later": "나중에 저장", - "Saved for later": "나중에 저장됨", - "Search": "찾다", - "Search GIFs": "GIF 검색", - "search-results-header-filter-source-button-label--channels": "채널", - "search-results-header-filter-source-button-label--messages": "메시지", - "search-results-header-filter-source-button-label--users": "사용자", - "Searching for {{ searchSourceType }}...": "{{ searchSourceType }} 검색 중...", - "Searching...": "수색...", - "searchResultsCount_one": "1개의 결과", - "searchResultsCount_other": "{{ count }}개 결과", - "See all options ({{count}})_one": "모든 옵션 보기 ({{count}})", - "See all options ({{count}})_other": "모든 옵션 보기 ({{count}})", - "Select a thread to continue the conversation": "대화를 계속하려면 스레드를 선택하세요", - "Select more than one option": "하나 이상의 선택지 선택", - "Select one": "하나 선택", - "Select one or more": "하나 이상 선택", - "Select up to {{count}}_one": "{{count}}개까지 선택", - "Select up to {{count}}_other": "{{count}}개까지 선택", - "Select your current location and optionally enable live location sharing": "현재 위치를 선택하고 필요 시 실시간 위치 공유를 활성화", - "Send": "보내다", - "Send a message": "메시지 보내기", - "Send a message to start the conversation": "대화를 시작하려면 메시지를 보내세요", - "Send Anyway": "어쨌든 보내기", - "Send direct message": "다이렉트 메시지 보내기", - "Send message request failed": "메시지 보내기 요청 실패", - "Send poll": "투표 보내기", - "Sending...": "배상중...", - "Sent": "전송됨", - "Share": "공유", - "Share a file to see it here": "파일을 공유하면 여기에 표시됩니다", - "Share a photo or video to see it here": "사진이나 동영상을 공유하면 여기에 표시됩니다", - "Share live location for": "라이브 위치 공유", - "Share Location": "위치 공유", - "Shared live location": "공유된 라이브 위치", - "Shared location": "공유된 위치", - "Show all": "모두 보기", - "Shuffle": "셔플", - "size limit": "크기 제한", - "Slow Mode ON": "슬로우 모드 켜짐", - "Slow mode, wait {{ seconds }}s...": "슬로우 모드, {{ seconds }}초 기다려 주세요...", - "Some of the files will not be accepted": "일부 파일은 허용되지 않을 수 있습니다", - "Start typing to search": "검색하려면 입력을 시작하세요", - "Stop sharing": "공유 중지", - "Submit": "제출", - "Suggest a new option to add to this poll": "이 투표에 추가할 새 옵션 제안", - "Suggest an option": "옵션 제안", - "Tap to remove": "제거하려면 탭하세요", - "Tap to remove: {{ reactionName }}": "제거하려면 탭하세요: {{ reactionName }}", - "Thinking...": "생각 중...", - "this content could not be displayed": "이 콘텐츠를 표시할 수 없습니다", - "This field cannot be empty or contain only spaces": "이 필드는 비워둘 수 없으며 공백만 포함할 수도 없습니다", - "This message did not meet our content guidelines": "이 메시지는 콘텐츠 가이드라인을 충족하지 않습니다.", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "이 사용자가 다시 메시지를 보낼 수 있습니다.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "스레드", - "Thread has not been found": "스레드를 찾을 수 없습니다", - "Thread reply": "스레드 답장", - "Thread Reply": "스레드 답장", - "ThreadListUnseenThreadsBanner/loading": "로딩 중...", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "읽지 않은 스레드 {{ count }}개", - "Threads": "스레드", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[어제]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[오늘]\", \"nextDay\": \"[내일]\", \"lastDay\": \"[어제]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[지난] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "{{ count }}일 전", - "timestamp/relativeToday": "오늘", - "timestamp/relativeWeeksAgo": "{{ count }}주 전", - "timestamp/relativeYesterday": "어제", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[오늘] HH:mm\", \"nextDay\": \"[내일] HH:mm\", \"lastDay\": \"[어제] HH:mm\", \"nextWeek\": \"dddd HH:mm\", \"lastWeek\": \"[지난] dddd HH:mm\", \"sameElse\": \"ddd, D MMM HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "브라우저에서 카메라 액세스를 허용하여 녹음을 시작합니다", - "To start recording, allow the microphone access in your browser": "브라우저에서 마이크로폰 액세스를 허용하여 녹음을 시작합니다", - "totalVoteCount_other": "총 {{ count }}표", - "Translated": "번역됨", - "Translated from {{ language }}": "{{ language }}(으)로 번역됨", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "2에서 10 사이의 숫자를 입력하세요", - "Type your message": "메시지 입력", - "Unarchive": "아카이브 해제", - "unban-command-args": "[@사용자이름]", - "unban-command-description": "사용자 차단 해제", - "Unblock": "차단 해제", - "Unblock user": "사용자 차단 해제", - "Unblock User": "사용자 차단 해제", - "unknown error": "알 수 없는 오류", - "Unmute": "음소거 해제", - "Unmute chat": "채팅 음소거 해제", - "Unmute user": "사용자 음소거 해제", - "unmute-command-args": "[@사용자이름]", - "unmute-command-description": "사용자 음소거 해제", - "Unpin": "핀 해제", - "Unread messages": "읽지 않은 메시지", - "Unsupported attachment": "지원되지 않는 첨부 파일", - "unsupported file type": "지원되지 않는 파일 형식", - "Update": "업데이트", - "Update the comment attached to your poll answer": "투표 답변에 첨부된 댓글 업데이트", - "Update your comment": "댓글 업데이트", - "Upload blocked": "업로드가 차단되었습니다", - "Upload error": "업로드 오류", - "Upload failed": "업로드에 실패했습니다", - "Upload Picture": "사진 업로드", - "Upload type: \"{{ type }}\" is not allowed": "업로드 유형: \"{{ type }}\"은(는) 허용되지 않습니다.", - "User blocked": "사용자가 차단됨", - "User muted": "사용자가 음소거되었습니다", - "User removed": "사용자가 제거되었습니다", - "User unblocked": "사용자 차단이 해제됨", - "User unmuted": "사용자 음소거가 해제되었습니다", - "User uploaded content": "사용자 업로드 콘텐츠", - "Video": "동영상", - "videoCount_other": "동영상 {{ count }}개", - "View": "보기", - "View {{count}} comments_one": "{{count}}개의 댓글 보기", - "View {{count}} comments_other": "{{count}}개의 댓글 보기", - "View all": "전체 보기", - "View member details for {{ member }}": "{{ member }} 멤버 상세 정보 보기", - "View original": "원문 보기", - "View results": "결과 보기", - "View translation": "번역 보기", - "Voice message": "음성 메시지", - "Voice message {{ duration }}": "음성 메시지 {{ duration }}", - "Voice message deleted": "음성 메시지가 삭제됨", - "voiceMessageCount_other": "음성 메시지 {{ count }}개", - "Vote ended": "투표 종료", - "Votes": "투표", - "Wait until all attachments have uploaded": "모든 첨부 파일이 업로드될 때까지 기다립니다.", - "Waiting for network…": "네트워크 대기 중…", - "You": "당신", - "You have no channels currently": "현재 채널이 없습니다.", - "You've reached the maximum number of files": "최대 파일 수에 도달했습니다." -} diff --git a/src/i18n/nl.json b/src/i18n/nl.json deleted file mode 100644 index 7f50824b57..0000000000 --- a/src/i18n/nl.json +++ /dev/null @@ -1,710 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} en {{ moreCount }} meer", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} en {{ lastUser }}", - "{{ count }} files_one": "{{ count }} bestand", - "{{ count }} files_other": "{{ count }} bestanden", - "{{ count }} members_one": "{{ count }} lid", - "{{ count }} members_other": "{{ count }} leden", - "{{ count }} members added_one": "{{ count }} lid toegevoegd", - "{{ count }} members added_other": "{{ count }} leden toegevoegd", - "{{ count }} people are typing_one": "{{ count }} persoon typt", - "{{ count }} people are typing_many": "{{ count }} personen typen", - "{{ count }} people are typing_other": "{{ count }} personen typen", - "{{ count }} photos_one": "{{ count }} foto", - "{{ count }} photos_other": "{{ count }} foto's", - "{{ count }} reactions_one": "{{ count }} reactie", - "{{ count }} reactions_other": "{{ count }} reacties", - "{{ count }} videos_one": "{{ count }} video", - "{{ count }} videos_other": "{{ count }} video's", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} en {{ secondUser }}", - "{{ imageCount }} more": "+{{ imageCount }}", - "{{ member }} will be able to message you again.": "{{ member }} kan je weer berichten sturen.", - "{{ member }} won't be able to message you anymore.": "{{ member }} kan je geen berichten meer sturen.", - "{{ memberCount }} members": "{{ memberCount }} deelnemers", - "{{ typing }} are typing": "{{ typing }} typen", - "{{ typing }} is typing": "{{ typing }} typt", - "{{ user }} has been muted": "{{ user }} is gedempt", - "{{ user }} has been unmuted": "{{ user }} is niet meer gedempt", - "{{ user }} is typing...": "{{ user }} is aan het typen...", - "{{ users }} and {{ user }} are typing...": "{{ users }} en {{ user }} zijn aan het typen...", - "{{ users }} and more are typing...": "{{ users }} en meer zijn aan het typen...", - "{{ watcherCount }} online": "{{ watcherCount }} online", - "{{count}} new messages_one": "{{count}} nieuw bericht", - "{{count}} new messages_other": "{{count}} nieuwe berichten", - "{{count}} unread_one": "{{count}} ongelezen", - "{{count}} unread_other": "{{count}} ongelezen", - "{{count}} votes_one": "{{count}} stem", - "{{count}} votes_other": "{{count}} stemmen", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+{{count}} meer optie", - "+{{count}} more options_other": "+{{count}} meer opties", - "🏙 Attachment...": "🏙 Bijlage...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} heeft gemaakt: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} heeft gestemd: {{pollOptionText}}", - "📍Shared location": "📍Gedeelde locatie", - "Actions": "Actions", - "Add": "Toevoegen", - "Add {{ count }} members_one": "{{ count }} lid toevoegen", - "Add {{ count }} members_other": "{{ count }} leden toevoegen", - "Add a comment": "Voeg een opmerking toe", - "Add a comment to your poll answer": "Voeg een reactie toe aan je pollantwoord", - "Add an option": "Voeg een optie toe", - "Add channel members": "Kanaalleden toevoegen", - "Add members": "Leden toevoegen", - "Add reaction": "Reactie toevoegen", - "Admin": "Beheerder", - "All results loaded": "Alle resultaten geladen", - "Allow access to camera": "Toegang tot camera toestaan", - "Allow access to microphone": "Toegang tot microfoon toestaan", - "Allow comments": "Sta opmerkingen toe", - "Allow option suggestion": "Sta optie-suggesties toe", - "Allow others to add comments": "Sta anderen toe om opmerkingen toe te voegen", - "Already a member": "Al lid", - "Also send as a direct message": "Ook als direct bericht versturen", - "Also send in channel": "Ook in kanaal versturen", - "Also sent in channel": "Ook in kanaal verzonden", - "An error has occurred during recording": "Er is een fout opgetreden tijdens het opnemen", - "An error has occurred during the recording processing": "Er is een fout opgetreden tijdens de verwerking van de opname", - "Anonymous": "Anoniem", - "Anonymous poll": "Anonieme peiling", - "Archive": "Archief", - "Are you sure you want to delete this message?": "Weet je zeker dat je dit bericht wilt verwijderen?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} bijlage", - "aria/{{ count }} attachment_other": "{{ count }} bijlagen", - "aria/{{ count }} search results_one": "{{ count }} zoekresultaat", - "aria/{{ count }} search results_other": "{{ count }} zoekresultaten", - "aria/{{ count }} suggestions_one": "{{ count }} suggestie", - "aria/{{ count }} suggestions_other": "{{ count }} suggesties", - "aria/{{ count }} unread message_one": "{{ count }} ongelezen bericht", - "aria/{{ count }} unread message_other": "{{ count }} ongelezen berichten", - "aria/{{ setting }} disabled": "{{ setting }} uitgeschakeld", - "aria/{{ setting }} enabled": "{{ setting }} ingeschakeld", - "aria/Active": "Actief", - "aria/Animated GIF": "Geanimeerde GIF", - "aria/Animated GIF: {{ title }}": "Geanimeerde GIF: {{ title }}", - "aria/Attachment": "Bijlage", - "aria/Attachment {{ attachmentType }}": "Bijlage {{ attachmentType }}", - "aria/Attachment Actions": "Bijlageacties", - "aria/audio": "audio", - "aria/Audio position {{ elapsed }} of {{ duration }}": "Audiopositie {{ elapsed }} van {{ duration }}", - "aria/Audio position {{ progress }} percent": "Audiopositie {{ progress }} procent", - "aria/Back to attachments": "Terug naar bijlagen", - "aria/Back to parent menu button": "Terug naar bovenliggend menu knop", - "aria/Block User": "Gebruiker blokkeren", - "aria/Bookmark Message": "Bericht bookmarken", - "aria/Cancel recording": "Opname annuleren", - "aria/Cancel Reply": "Antwoord annuleren", - "aria/Channel Actions": "Kanaalacties", - "aria/Channel details": "Kanaaldetails", - "aria/Channel list": "Kanaallijst", - "aria/Chat view controls": "Bedieningselementen chatweergave", - "aria/Chat: {{ channelName }}": "Chat: {{ channelName }}", - "aria/Clear search": "Zoekopdracht wissen", - "aria/Close callout dialog": "Calloutdialoog sluiten", - "aria/Close thread": "Draad sluiten", - "aria/Collapse sidebar": "Zijbalk samenklappen", - "aria/Command activated: {{ command }}": "Opdracht geactiveerd: {{ command }}", - "aria/Command Suggestions": "Opdrachtsuggesties", - "aria/Complete recording": "Opname voltooien", - "aria/Copy Message Text": "Berichttekst kopiëren", - "aria/Decrease value": "Waarde verlagen", - "aria/Delete Message": "Bericht verwijderen", - "aria/Delivered": "Bezorgd", - "aria/Delivery status: {{ deliveryStatus }}": "Bezorgstatus: {{ deliveryStatus }}", - "aria/Dismiss notification": "Melding sluiten", - "aria/Download attachment": "Bijlage downloaden", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "'{{ option }}' losgelaten op positie {{ position }}.", - "aria/Edit Message": "Bericht bewerken", - "aria/Emoji picker": "Emoji kiezer", - "aria/Emoji Suggestions": "Emoji-suggesties", - "aria/Exit search": "Zoeken afsluiten", - "aria/Expand sidebar": "Zijbalken uitvouwen", - "aria/file": "bestand", - "aria/File upload": "Bestand uploaden", - "aria/Flag Message": "Bericht markeren", - "aria/GIF": "GIF", - "aria/Giphy actions": "Giphy-acties", - "aria/Giphy canceled": "Giphy geannuleerd", - "aria/Giphy image changed": "Giphy-afbeelding gewijzigd", - "aria/Giphy image changed: {{ title }}": "Giphy-afbeelding gewijzigd: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Giphy-voorbeeld, alleen zichtbaar voor jou. Gebruik de acties Verzenden, Shuffelen of Annuleren.", - "aria/Giphy sent": "Giphy verzonden", - "aria/Go back": "Ga terug", - "aria/image": "afbeelding", - "aria/Image failed to load": "Afbeelding laden mislukt", - "aria/Increase value": "Waarde verhogen", - "aria/Jump to latest message": "Ga naar laatste bericht", - "aria/Jump to quoted message": "Ga naar geciteerd bericht", - "aria/Last activity: {{ time }}": "Laatste activiteit: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "Laatste bericht van {{ sender }}: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "Laatste bericht: {{ messagePreview }}", - "aria/Mark Message Unread": "Markeren als ongelezen", - "aria/Mark messages as read": "Markeer berichten als gelezen", - "aria/Mention Suggestions": "Vermeldingssuggesties", - "aria/Message Actions": "Berichtacties", - "aria/Message from {{ user }},": "Bericht van {{ user }},", - "aria/Message input": "Berichtinvoer", - "aria/Message with attachments": "Bericht met bijlagen", - "aria/Message,": "Bericht,", - "aria/Mute User": "Gebruiker dempen", - "aria/Next page": "Volgende pagina", - "aria/No search results found": "Geen zoekresultaten gevonden", - "aria/Notifications": "Meldingen", - "aria/Open Attachment Selector": "Open bijlage selector", - "aria/Open Channel Actions Menu": "Kanaalactiemenu openen", - "aria/Open channel details": "Kanaaldetails openen", - "aria/Open channels view": "Kanaalweergave openen", - "aria/Open image shared by {{ name }}": "Door {{ name }} gedeelde afbeelding openen", - "aria/Open Message Actions Menu": "Menu voor berichtacties openen", - "aria/Open Reaction Selector": "Reactiekiezer openen", - "aria/Open Thread": "Draad openen", - "aria/Open threads view": "Threadweergave openen", - "aria/Open threads view with unread threads_one": "Threadweergave openen, {{ count }} ongelezen thread", - "aria/Open threads view with unread threads_other": "Threadweergave openen, {{ count }} ongelezen threads", - "aria/Open video shared by {{ name }}": "Door {{ name }} gedeelde video openen", - "aria/Opened channel: {{ name }}": "Kanaal geopend: {{ name }}", - "aria/Opened thread in {{ name }}": "Thread geopend in {{ name }}", - "aria/Option {{ position }}": "Optie {{ position }}", - "aria/Options can now be reordered and removed.": "Opties kunnen nu worden herschikt en verwijderd.", - "aria/Pause": "Pauzeren", - "aria/Pause recording": "Opname pauzeren", - "aria/Percent complete": "{{percent}} procent voltooid", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "'{{ option }}' opgepakt. Gebruik de pijltoetsen om opnieuw te ordenen. Druk op spatie of tab om los te laten.", - "aria/Pin Message": "Bericht vastmaken", - "aria/Play": "Afspelen", - "aria/Poll dialog opened": "Peilingdialoogvenster geopend", - "aria/Poll sent": "Peiling verzonden", - "aria/Poll: {{ pollName }}": "Peiling: {{ pollName }}", - "aria/Press Enter to start typing": "Druk op Enter om te beginnen met typen", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "Druk op de spatiebalk om deze optie te selecteren, gebruik de pijltoetsen omhoog en omlaag om deze te verplaatsen en druk daarna opnieuw op de spatiebalk om de selectie op te heffen.", - "aria/Previous page": "Vorige pagina", - "aria/Quote Message": "Bericht citeren", - "aria/Reaction list": "Reactielijst", - "aria/Read": "Gelezen", - "aria/Recording paused": "Opname gepauzeerd", - "aria/Recording resumed": "Opname hervat", - "aria/Recording started": "Opname gestart", - "aria/Remind Me Message": "Herinner mij", - "aria/Remove attachment": "Bijlage verwijderen", - "aria/Remove location attachment": "Locatie bijlage verwijderen", - "aria/Remove option: {{ option }}": "Optie verwijderen: {{ option }}", - "aria/Remove Reminder": "Herinnering verwijderen", - "aria/Remove Save For Later": "Verwijder 'Bewaren voor later'", - "aria/Removed option {{ option }}": "Optie {{ option }} verwijderd", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "'{{ option }}' opnieuw ordenen, positie {{ position }} van {{ total }}", - "aria/Reorder option {{ position }}": "Optie {{ position }} opnieuw ordenen", - "aria/Resend Message": "Bericht opnieuw verzenden", - "aria/Resume recording": "Opname hervatten", - "aria/Retry upload": "Upload opnieuw proberen", - "aria/Review bounced message": "Controleer teruggestuurd bericht", - "aria/Search cleared": "Zoekopdracht gewist", - "aria/Search results": "Zoekresultaten", - "aria/Search results header filter button": "Zoekresultaten header filter knop", - "aria/Search results header filter button for: {{ source }}": "Filterknop koptekst zoekresultaten voor: {{ source }}", - "aria/Seek audio position": "Audiopositie zoeken", - "aria/Select Reaction: {{ reactionName }}": "Reactie selecteren: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "Gebruikerskanaal selecteren: {{ name }}", - "aria/Send": "Verzenden", - "aria/Sent": "Verzonden", - "aria/Shared a link": "Link gedeeld", - "aria/Shared a link with title: {{ linkTitle }}": "Link gedeeld met titel: {{ linkTitle }}", - "aria/Shared location": "Gedeelde locatie", - "aria/Show preview": "Voorbeeld tonen", - "aria/Start recording audio": "Audio-opname starten", - "aria/Stop AI Generation": "AI-generatie stoppen", - "aria/Submenu": "Submenu", - "aria/Suggestions": "Suggesties", - "aria/There are no messages in this chat.": "Er zijn geen berichten in deze chat", - "aria/This option can be reordered and removed.": "Deze optie kan worden herschikt en verwijderd.", - "aria/Thread list": "Threadlijst", - "aria/Thread: {{ messagePreview }}": "Thread: {{ messagePreview }}", - "aria/Unblock User": "Gebruiker deblokkeren", - "aria/Unmute User": "Dempen opheffen", - "aria/Unpin Message": "Losmaken", - "aria/User selected: {{ user }}": "Gebruiker geselecteerd: {{ user }}", - "aria/video": "video", - "aria/voice message": "spraakbericht", - "aria/Voice message sent": "Spraakbericht verzonden", - "aria/Voice recording attached": "Spraakopname bijgevoegd", - "Ask a question": "Stel een vraag", - "Attach": "Bijvoegen", - "Attach files": "Bijlage toevoegen", - "Attachment": "Bijlage", - "Attachment upload blocked due to {{reason}}": "Bijlage upload geblokkeerd vanwege {{reason}}", - "Attachment upload failed due to {{reason}}": "Bijlage upload mislukt vanwege {{reason}}", - "Back": "Terug", - "ban-command-args": "[@gebruikersnaam] [tekst]", - "ban-command-description": "Een gebruiker verbannen", - "Block user": "Gebruiker blokkeren", - "Block User": "Gebruiker blokkeren", - "Browse channel members": "Kanaalleden bekijken", - "Browse pinned messages": "Vastgemaakte berichten bekijken", - "Cancel": "Annuleer", - "Cannot seek in the recording": "Kan niet zoeken in de opname", - "Changes saved": "Wijzigingen opgeslagen", - "Channel archived": "Kanaal gearchiveerd", - "Channel members": "Kanaalleden", - "Channel Missing": "Kanaal niet gevonden", - "Channel muted": "Kanaal gedempt", - "Channel pinned": "Kanaal vastgezet", - "Channel unarchived": "Kanaal uit archief gehaald", - "Channel unmuted": "Dempen van kanaal opgeheven", - "Channel unpinned": "Kanaal losgemaakt", - "Channels": "Kanalen", - "Chat deleted": "Chat deleted", - "Chats": "Chats", - "Choose between 2 to 10 options": "Kies tussen 2 en 10 opties", - "Close": "Sluit", - "Close dialog": "Dialoog sluiten", - "Close emoji picker": "Sluit de emoji-kiezer", - "Command not available while editing": "Opdracht niet beschikbaar tijdens bewerken", - "Command not available while replying": "Opdracht niet beschikbaar tijdens beantwoorden", - "Commands": "Commando's", - "Commands matching": "Bijpassende opdrachten", - "Connection failure, reconnecting now...": "Verbindingsfout, opnieuw verbinden...", - "Contact info": "Contactgegevens", - "Contact name": "Contactnaam", - "Copy Message": "Bericht kopiëren", - "Create": "Maak", - "Create a question, add options, and configure poll settings": "Maak een vraag, voeg opties toe en stel de pollinstellingen in", - "Create poll": "Maak peiling", - "Current location": "Huidige locatie", - "Delete": "Verwijder", - "Delete chat": "Chat verwijderen", - "Delete for me": "Voor mij verwijderen", - "Delete message": "Bericht verwijderen", - "Delivered": "Afgeleverd", - "Direct message": "Direct bericht", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "Wil je deze peiling nu beëindigen? Niemand kan daarna meer op deze peiling stemmen.", - "Download {{ fileName }}": "{{ fileName }} downloaden", - "Download All": "Alles downloaden", - "Download Attachment": "Bijlage downloaden", - "Download attachment {{ name }}": "Bijlage {{ name }} downloaden", - "Download attachment {{ number }}": "Bijlage {{ number }} downloaden", - "Drag your files here": "Sleep je bestanden hier naartoe", - "Drag your files here to add to your post": "Sleep je bestanden hier naartoe om aan je bericht toe te voegen", - "Due {{ timeLeft }}": "Vervallen in {{ timeLeft }}", - "Due since {{ dueSince }}": "Vervallen sinds {{ dueSince }}", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "Bewerken", - "Edit chat data": "Chatgegevens bewerken", - "Edit contact": "Contact bewerken", - "Edit group": "Groep bewerken", - "Edit Message": "Bericht bewerken", - "Edit message request failed": "Verzoek om bericht bewerken mislukt", - "Edited": "Bewerkt", - "Emoji matching": "Emoji-overeenkomsten", - "Empty message...": "Leeg bericht...", - "End": "Einde", - "End poll": "Peiling beëindigen", - "End this poll?": "Peiling beëindigen?", - "End vote": "Einde stem", - "Enforce unique vote is enabled": "Unieke stem is ingeschakeld", - "Error": "Fout", - "Error · Unsent": "Fout · niet verzonden", - "Error adding flag": "Fout bij toevoegen van vlag", - "Error adding members": "Error adding members", - "Error blocking user": "Fout bij blokkeren van gebruiker", - "Error connecting to chat, refresh the page to try again.": "Fout bij het verbinden, ververs de pagina om nogmaals te proberen", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "Fout bij verwijderen van bericht", - "Error fetching reactions": "Fout bij het laden van reacties", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "Fout bij markeren van bericht als ongelezen. Kan geen oudere ongelezen berichten markeren dan de nieuwste 100 kanaalberichten.", - "Error muting a user ...": "Fout bij het muten van de gebruiker", - "Error muting channel": "Fout bij dempen van kanaal", - "Error muting user": "Fout bij dempen van gebruiker", - "Error opening direct message": "Fout bij het openen van het directe bericht", - "Error pinning message": "Fout bij vastzetten van bericht", - "Error removing members": "Fout bij het verwijderen van de leden", - "Error removing message pin": "Fout bij verwijderen van berichtpin", - "Error removing user": "Fout bij het verwijderen van de gebruiker", - "Error reproducing the recording": "Fout bij het afspelen van de opname", - "Error starting recording": "Fout bij het starten van de opname", - "Error unblocking user": "Fout bij deblokkeren van gebruiker", - "Error unmuting a user ...": "Fout bij het unmuten van de gebruiker", - "Error unmuting channel": "Fout bij opheffen van kanaaldemping", - "Error unmuting user": "Fout bij opheffen van gebruikersdemping", - "Error uploading attachment": "Fout bij het uploaden van de bijlage", - "Error uploading file": "Fout bij uploaden bestand", - "Error uploading image": "Fout bij uploaden afbeelding", - "Error: {{ errorMessage }}": "Fout: {{ errorMessage }}", - "Exit command {{ command }}": "Opdracht verlaten {{ command }}", - "Failed to block user": "Gebruiker blokkeren mislukt", - "Failed to create the poll": "Fout bij het maken van de peiling", - "Failed to create the poll due to {{reason}}": "Peiling kon niet worden aangemaakt vanwege {{reason}}", - "Failed to delete the message": "Bericht verwijderen mislukt", - "Failed to end the poll": "Peiling kon niet worden beëindigd", - "Failed to end the poll due to {{reason}}": "Peiling kon niet worden beëindigd vanwege {{reason}}", - "Failed to jump to the first unread message": "Niet gelukt om naar het eerste ongelezen bericht te springen", - "Failed to leave channel": "Kanaal verlaten mislukt", - "Failed to load channels": "Kanalen konden niet worden geladen", - "Failed to load more channels": "Meer kanalen konden niet worden geladen", - "Failed to mark channel as read": "Kanaal kon niet als gelezen worden gemarkeerd", - "Failed to play the recording": "Kan de opname niet afspelen", - "Failed to retrieve location": "Locatie kon niet worden opgehaald", - "Failed to save changes": "Wijzigingen opslaan mislukt", - "Failed to share location": "Locatie kon niet worden gedeeld", - "Failed to update channel archive status": "Archiefstatus van kanaal kon niet worden bijgewerkt", - "Failed to update channel mute status": "Muteerstatus van kanaal kon niet worden bijgewerkt", - "Failed to update channel pinned status": "Vastgezette status van kanaal kon niet worden bijgewerkt", - "File": "Bestand", - "File is required for upload attachment": "Bestand is vereist voor het uploaden van een bijlage", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "Bestand is te groot: {{ size }}, maximale uploadgrootte is {{ limit }}", - "File too large": "Bestand is te groot", - "fileCount_one": "1 bestand", - "fileCount_other": "{{ count }} bestanden", - "Files": "Bestanden", - "Flag": "Markeer", - "Generating...": "Genereren...", - "giphy-command-args": "[tekst]", - "giphy-command-description": "Plaats een willekeurige gif in het kanaal", - "Go back": "Terug", - "Group info": "Groepsinformatie", - "Group name": "Groepsnaam", - "Hide who voted": "Verberg wie heeft gestemd", - "Image": "Afbeelding", - "imageCount_one": "Afbeelding", - "imageCount_other": "{{ count }} afbeeldingen", - "Instant commands": "Snelle opdrachten", - "language/af": "Afrikaans", - "language/am": "Amhaars", - "language/ar": "Arabisch", - "language/az": "Azerbeidzjaans", - "language/bg": "Bulgaars", - "language/bn": "Bengaals", - "language/bs": "Bosnisch", - "language/cs": "Tsjechisch", - "language/da": "Deens", - "language/de": "Duits", - "language/el": "Grieks", - "language/en": "Engels", - "language/es": "Spaans", - "language/es-MX": "Spaans (Mexico)", - "language/et": "Estlands", - "language/fa": "Perzisch", - "language/fa-AF": "Dari", - "language/fi": "Fins", - "language/fr": "Frans", - "language/fr-CA": "Frans (Canada)", - "language/ha": "Hausa", - "language/he": "Hebreeuws", - "language/hi": "Hindi", - "language/hr": "Kroatisch", - "language/ht": "Haïtiaans Creools", - "language/hu": "Hongaars", - "language/id": "Indonesisch", - "language/it": "Italiaans", - "language/ja": "Japans", - "language/ka": "Georgisch", - "language/ko": "Koreaans", - "language/lt": "Litouws", - "language/lv": "Letlands", - "language/ms": "Maleis", - "language/nl": "Nederlands", - "language/no": "Noors", - "language/pl": "Pools", - "language/ps": "Pasjtoe", - "language/pt": "Portugees", - "language/ro": "Roemeens", - "language/ru": "Russisch", - "language/sk": "Slowaaks", - "language/sl": "Sloveens", - "language/so": "Somali", - "language/sq": "Albanees", - "language/sr": "Servisch", - "language/sv": "Zweeds", - "language/sw": "Swahili", - "language/ta": "Tamil", - "language/th": "Thai", - "language/tl": "Tagalog", - "language/tr": "Turks", - "language/uk": "Oekraïens", - "language/ur": "Urdu", - "language/vi": "Vietnamees", - "language/zh": "Chinees (vereenvoudigd)", - "language/zh-TW": "Chinees (traditioneel)", - "Last seen {{ timestamp }}": "Laatst gezien {{ timestamp }}", - "Leave Channel": "Kanaal verlaten", - "Leave chat": "Kanaal verlaten", - "Left channel": "Kanaal verlaten", - "Let others add options": "Laat anderen opties toevoegen", - "Limit votes per person": "Stemmen per persoon beperken", - "Link": "Link", - "linkCount_one": "Link", - "linkCount_other": "{{ count }} links", - "live": "live", - "Live for {{duration}}": "Live voor {{duration}}", - "Live location": "Live locatie", - "Live until {{ timestamp }}": "Live tot {{ timestamp }}", - "Load more": "Meer laden", - "Local upload attachment missing local id": "Lokale uploadbijlage mist lokale id", - "Location": "Locatie", - "Location sharing ended": "Locatie delen beëindigd", - "Location: {{ coordinates }}": "Locatie: {{ coordinates }}", - "Manage channel": "Kanaal beheren", - "Manage members": "Leden beheren", - "Mark as unread": "Markeren als ongelezen", - "Maximum number of votes (from 2 to 10)": "Maximaal aantal stemmen (van 2 tot 10)", - "Maximum votes per person": "Maximum aantal stemmen per persoon", - "Member detail": "Lidgegevens", - "mention/Channel": "Kanaal", - "mention/Channel Description": "Iedereen in dit kanaal informeren", - "mention/Here": "Hier", - "mention/Here Description": "Alle online leden in dit kanaal informeren", - "Menu": "Menu", - "Message deleted": "Bericht verwijderd", - "Message Failed · Click to try again": "Bericht mislukt, klik om het nogmaals te proberen", - "Message Failed · Unauthorized": "Bericht mislukt, ongeautoriseerd", - "Message failed to send": "Bericht kon niet worden verzonden", - "Message has been successfully flagged": "Bericht is succesvol gemarkeerd", - "Message marked as unread": "Bericht gemarkeerd als ongelezen", - "Message pinned": "Bericht vastgezet", - "Message unpinned": "Bericht losgemaakt", - "Message was blocked by moderation policies": "Bericht is geblokkeerd door moderatiebeleid", - "Messages have been marked unread.": "Berichten zijn gemarkeerd als ongelezen.", - "Missing permissions to upload the attachment": "Missende toestemmingen om de bijlage te uploaden", - "Moderator": "Moderator", - "Multiple votes": "Meerdere stemmen", - "Mute": "Dempen", - "Mute chat": "Chat dempen", - "Mute user": "Gebruiker dempen", - "mute-command-args": "[@gebruikersnaam]", - "mute-command-description": "Een gebruiker dempen", - "network error": "netwerkfout", - "New": "Nieuwe", - "New message from {{user}}": "Nieuw bericht van {{user}}", - "New Messages!": "Nieuwe Berichten!", - "Next": "Volgende", - "Next image": "Volgende afbeelding", - "No chats here yet…": "Nog geen chats hier...", - "No conversations yet": "Nog geen gesprekken", - "No files": "Geen bestanden", - "No items exist": "Er zijn geen items", - "No member found": "Geen lid gevonden", - "No messages found": "Geen berichten gevonden", - "No photos or videos": "Geen foto's of video's", - "No pinned messages": "Geen vastgemaakte berichten", - "No results found": "Geen resultaten gevonden", - "No user found": "Geen gebruiker gevonden", - "Nobody will be able to vote in this poll anymore.": "Niemand kan meer stemmen in deze peiling.", - "Nothing yet...": "Nog niets ...", - "Notify all {{ role }} members": "Alle leden met rol {{ role }} informeren", - "Offline": "Offline", - "Ok": "Oké", - "Online": "Online", - "Only numbers are allowed": "Alleen nummers zijn toegestaan", - "Only visible to you": "Alleen zichtbaar voor jou", - "Open emoji picker": "Emoji-kiezer openen", - "Open gallery at image {{ index }}": "Galerij openen bij afbeelding {{ index }}", - "Open image in gallery": "Afbeelding openen in galerij", - "Open location in a map": "Locatie op een kaart openen", - "Open members actions": "Open members actions", - "Open menu": "Menu openen", - "Option already exists": "Optie bestaat al", - "Option is empty": "Optie is leeg", - "Options": "Opties", - "Original": "Origineel", - "Owner": "Eigenaar", - "People matching": "Mensen die matchen", - "Photo": "Foto", - "Photos & videos": "Foto's en video's", - "Pin": "Vastmaken", - "Pin a message to see it here": "Maak een bericht vast om het hier te zien", - "Pinned by {{ name }}": "Vastgemaakt door {{ name }}", - "Pinned by You": "Door jou vastgezet", - "Pinned message": "Vastgemaakt bericht", - "Pinned messages": "Vastgemaakte berichten", - "placeholder/PollComment": "Jouw reactie", - "placeholder/PollOptionSuggestion": "Voer een nieuwe optie in", - "Play video": "Video afspelen", - "Playback speed {{ rate }}x": "Afspeelsnelheid {{ rate }}x", - "Poll": "Peiling", - "Poll comments": "Peiling opmerkingen", - "Poll ended": "Peiling beëindigd", - "Poll options": "Peiling opties", - "Poll results": "Peiling resultaten", - "Poll sent": "Peiling verzonden", - "Previous": "Vorige", - "Previous image": "Vorige afbeelding", - "Question": "Vraag", - "Question {{ optionOrderNumber}}": "Vraag {{ optionOrderNumber}}", - "Question is required": "Vraag is verplicht", - "Quote Reply": "Citaatantwoord", - "Reached the vote limit. Remove an existing vote first.": "Stemlimiet bereikt. Verwijder eerst een bestaande stem.", - "Recording format is not supported and cannot be reproduced": "Opnameformaat wordt niet ondersteund en kan niet worden gereproduceerd", - "Remind me": "Herinner me", - "Remind Me": "Herinner mij", - "Reminder set": "Herinnering ingesteld", - "Remove": "Verwijderen", - "Remove {{ count }} members_one": "{{ count }} lid verwijderen", - "Remove {{ count }} members_other": "{{ count }} leden verwijderen", - "Remove {{ member }} from this channel?": "{{ member }} uit dit kanaal verwijderen?", - "Remove channel members": "Kanaalleden verwijderen", - "Remove reminder": "Herinnering verwijderen", - "Remove save for later": "Verwijder 'Bewaren voor later'", - "Remove user": "Gebruiker verwijderen", - "Removed {{ count }} members_one": "{{ count }} lid verwijderd", - "Removed {{ count }} members_other": "{{ count }} leden verwijderd", - "Replied to a thread": "Heeft gereageerd in een thread", - "Reply": "Antwoord", - "Reply to {{ authorName }}": "Antwoord aan {{ authorName }}", - "Reply to a message to start a thread": "Beantwoord een bericht om een thread te starten", - "Reply to Message": "Antwoord op bericht", - "replyCount_one": "1 antwoord", - "replyCount_other": "{{ count }} antwoorden", - "Resend": "Opnieuw verzenden", - "Retry upload": "Upload opnieuw proberen", - "Review all options available in this poll": "Bekijk alle beschikbare opties in deze poll", - "Review comments submitted with poll answers": "Bekijk reacties die met pollantwoorden zijn ingediend", - "Review poll results and open an option to see detailed votes": "Bekijk pollresultaten en open een optie om gedetailleerde stemmen te zien", - "Review this message and choose whether to delete it, edit it, or send it anyway": "Bekijk dit bericht en kies of je het verwijdert, bewerkt of toch verstuurt", - "Review who voted for this option": "Bekijk wie op deze optie heeft gestemd", - "Save": "Opslaan", - "Save for later": "Bewaren voor later", - "Saved for later": "Bewaard voor later", - "Search": "Zoeken", - "Search GIFs": "GIF's zoeken", - "search-results-header-filter-source-button-label--channels": "kanalen", - "search-results-header-filter-source-button-label--messages": "berichten", - "search-results-header-filter-source-button-label--users": "gebruikers", - "Searching for {{ searchSourceType }}...": "Zoeken naar {{ searchSourceType }}...", - "Searching...": "Zoeken...", - "searchResultsCount_one": "1 resultaat", - "searchResultsCount_other": "{{ count }} resultaten", - "See all options ({{count}})_one": "Bekijk alle opties ({{count}})", - "See all options ({{count}})_other": "Bekijk alle opties ({{count}})", - "Select a thread to continue the conversation": "Selecteer een thread om het gesprek voort te zetten", - "Select more than one option": "Selecteer meer dan één optie", - "Select one": "Selecteer er een", - "Select one or more": "Selecteer een of meer", - "Select up to {{count}}_one": "Selecteer tot {{count}}", - "Select up to {{count}}_other": "Selecteer tot {{count}}", - "Select your current location and optionally enable live location sharing": "Selecteer je huidige locatie en schakel eventueel live locatiedeling in", - "Send": "Verstuur", - "Send a message": "Stuur een bericht", - "Send a message to start the conversation": "Stuur een bericht om het gesprek te beginnen", - "Send Anyway": "Toch versturen", - "Send direct message": "Direct bericht sturen", - "Send message request failed": "Verzoek om bericht te verzenden mislukt", - "Send poll": "Peiling versturen", - "Sending...": "Aan het verzenden...", - "Sent": "Verzonden", - "Share": "Delen", - "Share a file to see it here": "Deel een bestand om het hier te zien", - "Share a photo or video to see it here": "Deel een foto of video om deze hier te zien", - "Share live location for": "Live locatie delen voor", - "Share Location": "Locatie delen", - "Shared live location": "Gedeelde live locatie", - "Shared location": "Gedeelde locatie", - "Show all": "Toon alles", - "Shuffle": "Schudden", - "size limit": "grootte limiet", - "Slow Mode ON": "Langzame modus aan", - "Slow mode, wait {{ seconds }}s...": "Langzame modus, wacht {{ seconds }}s...", - "Slow wait, wait {{ seconds }}s": "Langzame modus, wacht {{ seconds }}s", - "Slow wait, wait {{ seconds }}s...": "Langzame modus, wacht {{ seconds }}s...", - "Some of the files will not be accepted": "Sommige bestanden zullen niet worden geaccepteerd", - "Start typing to search": "Begin met typen om te zoeken", - "Stop sharing": "Delen stoppen", - "Submit": "Versturen", - "Suggest a new option to add to this poll": "Stel een nieuwe optie voor om aan deze poll toe te voegen", - "Suggest an option": "Stel een optie voor", - "Tap to remove": "Tik om te verwijderen", - "Tap to remove: {{ reactionName }}": "Tik om te verwijderen: {{ reactionName }}", - "Thinking...": "Denken...", - "this content could not be displayed": "Deze inhoud kan niet weergegeven worden", - "This field cannot be empty or contain only spaces": "Dit veld mag niet leeg zijn of alleen spaties bevatten", - "This message did not meet our content guidelines": "Dit bericht voldeed niet aan onze inhoudsrichtlijnen", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "Deze gebruiker kan je weer berichten sturen.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "Draadje", - "Thread has not been found": "Draadje niet gevonden", - "Thread reply": "Draadje antwoord", - "Thread Reply": "Draadje antwoord", - "ThreadListUnseenThreadsBanner/loading": "Laden...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} ongelezen thread", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} ongelezen threads", - "Threads": "Discussies", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Gisteren]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Vandaag]\", \"nextDay\": \"[Morgen]\", \"lastDay\": \"[Gisteren]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Laatste] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "{{ count }} d geleden", - "timestamp/relativeToday": "Vandaag", - "timestamp/relativeWeeksAgo": "{{ count }} w geleden", - "timestamp/relativeYesterday": "Gisteren", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Vandaag] [om] HH:mm\", \"nextDay\": \"[Morgen] [om] HH:mm\", \"lastDay\": \"[Gisteren] [om] HH:mm\", \"nextWeek\": \"dddd [om] HH:mm\", \"lastWeek\": \"[afgelopen] dddd [om] HH:mm\", \"sameElse\": \"ddd, D MMM [om] HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "Om te beginnen met opnemen, sta toegang tot de camera toe in uw browser", - "To start recording, allow the microphone access in your browser": "Om te beginnen met opnemen, sta toegang tot de microfoon toe in uw browser", - "totalVoteCount_one": "1 stem in totaal", - "totalVoteCount_other": "{{ count }} stemmen in totaal", - "Translated": "Vertaald", - "Translated from {{ language }}": "Vertaald uit {{ language }}", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "Typ een getal van 2 tot 10", - "Type your message": "Type je bericht", - "Unarchive": "Uit archief halen", - "unban-command-args": "[@gebruikersnaam]", - "unban-command-description": "Een gebruiker debannen", - "Unblock": "Deblokkeren", - "Unblock user": "Gebruiker deblokkeren", - "Unblock User": "Gebruiker deblokkeren", - "unknown error": "onbekende fout", - "Unmute": "Dempen opheffen", - "Unmute chat": "Chat dempen opheffen", - "Unmute user": "Gebruiker dempen opheffen", - "unmute-command-args": "[@gebruikersnaam]", - "unmute-command-description": "Een gebruiker niet meer dempen", - "Unpin": "Losmaken", - "Unread messages": "Ongelezen berichten", - "Unsupported attachment": "Niet-ondersteunde bijlage", - "unsupported file type": "niet-ondersteund bestandstype", - "Update": "Bijwerken", - "Update the comment attached to your poll answer": "Werk de reactie bij die aan je pollantwoord is toegevoegd", - "Update your comment": "Werk je opmerking bij", - "Upload blocked": "Upload geblokkeerd", - "Upload error": "Uploadfout", - "Upload failed": "Upload mislukt", - "Upload Picture": "Afbeelding uploaden", - "Upload type: \"{{ type }}\" is not allowed": "Uploadtype: \"{{ type }}\" is niet toegestaan", - "User blocked": "Gebruiker geblokkeerd", - "User muted": "Gebruiker gedempt", - "User removed": "Gebruiker verwijderd", - "User unblocked": "Gebruiker gedeblokkeerd", - "User unmuted": "Gebruiker niet meer gedempt", - "User uploaded content": "Gebruikersgeüploade inhoud", - "Video": "Video", - "videoCount_one": "Video", - "videoCount_other": "{{ count }} video's", - "View": "Bekijken", - "View {{count}} comments_one": "Bekijk {{count}} opmerkingen", - "View {{count}} comments_other": "Bekijk {{count}} opmerkingen", - "View all": "Alles bekijken", - "View member details for {{ member }}": "Lidgegevens voor {{ member }} bekijken", - "View original": "Origineel bekijken", - "View results": "Bekijk resultaten", - "View translation": "Vertaling bekijken", - "Voice message": "Spraakbericht", - "Voice message {{ duration }}": "Spraakbericht {{ duration }}", - "Voice message deleted": "Spraakbericht verwijderd", - "voiceMessageCount_one": "Spraakbericht", - "voiceMessageCount_other": "{{ count }} spraakberichten", - "Vote ended": "Stemmen beëindigd", - "Votes": "Stemmen", - "Wait until all attachments have uploaded": "Wacht tot alle bijlagen zijn geüpload", - "Waiting for network…": "Wachten op netwerk…", - "You": "Jij", - "You have no channels currently": "Er zijn geen chats beschikbaar", - "You've reached the maximum number of files": "Je hebt het maximale aantal bestanden bereikt" -} diff --git a/src/i18n/pt.json b/src/i18n/pt.json deleted file mode 100644 index 3129e77c67..0000000000 --- a/src/i18n/pt.json +++ /dev/null @@ -1,739 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} e mais {{ moreCount }}", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} e {{ lastUser }}", - "{{ count }} files_one": "{{ count }} arquivo", - "{{ count }} files_many": "{{ count }} arquivos", - "{{ count }} files_other": "{{ count }} arquivos", - "{{ count }} members_one": "{{ count }} membro", - "{{ count }} members_many": "{{ count }} membros", - "{{ count }} members_other": "{{ count }} membros", - "{{ count }} members added_one": "{{ count }} membro adicionado", - "{{ count }} members added_many": "{{ count }} membros adicionados", - "{{ count }} members added_other": "{{ count }} membros adicionados", - "{{ count }} people are typing_one": "{{ count }} pessoa está digitando", - "{{ count }} people are typing_many": "{{ count }} pessoas estão digitando", - "{{ count }} people are typing_other": "{{ count }} pessoas estão digitando", - "{{ count }} photos_one": "{{ count }} foto", - "{{ count }} photos_many": "{{ count }} fotos", - "{{ count }} photos_other": "{{ count }} fotos", - "{{ count }} reactions_one": "{{ count }} reação", - "{{ count }} reactions_many": "{{ count }} reações", - "{{ count }} reactions_other": "{{ count }} reações", - "{{ count }} videos_one": "{{ count }} vídeo", - "{{ count }} videos_many": "{{ count }} vídeos", - "{{ count }} videos_other": "{{ count }} vídeos", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} e {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }} mais", - "{{ member }} will be able to message you again.": "{{ member }} poderá enviar mensagens para você novamente.", - "{{ member }} won't be able to message you anymore.": "{{ member }} não poderá mais enviar mensagens para você.", - "{{ memberCount }} members": "{{ memberCount }} membros", - "{{ typing }} are typing": "{{ typing }} estão digitando", - "{{ typing }} is typing": "{{ typing }} está digitando", - "{{ user }} has been muted": "{{ user }} foi silenciado", - "{{ user }} has been unmuted": "{{ user }} foi reativado", - "{{ user }} is typing...": "{{ user }} está digitando...", - "{{ users }} and {{ user }} are typing...": "{{ users }} e {{ user }} estão digitando...", - "{{ users }} and more are typing...": "{{ users }} e mais estão digitando...", - "{{ watcherCount }} online": "{{ watcherCount }} online", - "{{count}} new messages_one": "{{count}} nova mensagem", - "{{count}} new messages_many": "{{count}} novas mensagens", - "{{count}} new messages_other": "{{count}} novas mensagens", - "{{count}} unread_one": "{{count}} não lido", - "{{count}} unread_many": "{{count}} não lidos", - "{{count}} unread_other": "{{count}} não lidos", - "{{count}} votes_one": "{{count}} voto", - "{{count}} votes_many": "{{count}} votos", - "{{count}} votes_other": "{{count}} votos", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+{{count}} opção a mais", - "+{{count}} more options_many": "+{{count}} opções a mais", - "+{{count}} more options_other": "+{{count}} opções a mais", - "🏙 Attachment...": "🏙 Anexo...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} criou: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} votou: {{pollOptionText}}", - "📍Shared location": "📍Localização compartilhada", - "Actions": "Actions", - "Add": "Adicionar", - "Add {{ count }} members_one": "Adicionar {{ count }} membro", - "Add {{ count }} members_many": "Adicionar {{ count }} membros", - "Add {{ count }} members_other": "Adicionar {{ count }} membros", - "Add a comment": "Adicionar um comentário", - "Add a comment to your poll answer": "Adicione um comentário à sua resposta da enquete", - "Add an option": "Adicionar uma opção", - "Add channel members": "Adicionar membros ao canal", - "Add members": "Adicionar membros", - "Add reaction": "Adicionar reação", - "Admin": "Admin", - "All results loaded": "Todos os resultados carregados", - "Allow access to camera": "Permitir acesso à câmera", - "Allow access to microphone": "Permitir acesso ao microfone", - "Allow comments": "Permitir comentários", - "Allow option suggestion": "Permitir sugestão de opção", - "Allow others to add comments": "Permitir que outros adicionem comentários", - "Already a member": "Já é membro", - "Also send as a direct message": "Também enviar como mensagem direta", - "Also send in channel": "Também enviar no canal", - "Also sent in channel": "Também enviado no canal", - "An error has occurred during recording": "Ocorreu um erro durante a gravação", - "An error has occurred during the recording processing": "Ocorreu um erro durante o processamento da gravação", - "Anonymous": "Anônimo", - "Anonymous poll": "Enquete anônima", - "Archive": "Arquivar", - "Are you sure you want to delete this message?": "Tem certeza de que deseja excluir esta mensagem?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_many": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} anexo", - "aria/{{ count }} attachment_many": "{{ count }} anexos", - "aria/{{ count }} attachment_other": "{{ count }} anexos", - "aria/{{ count }} search results_one": "{{ count }} resultado de pesquisa", - "aria/{{ count }} search results_many": "{{ count }} resultados de pesquisa", - "aria/{{ count }} search results_other": "{{ count }} resultados de pesquisa", - "aria/{{ count }} suggestions_one": "{{ count }} sugestão", - "aria/{{ count }} suggestions_many": "{{ count }} sugestões", - "aria/{{ count }} suggestions_other": "{{ count }} sugestões", - "aria/{{ count }} unread message_one": "{{ count }} mensagem não lida", - "aria/{{ count }} unread message_many": "{{ count }} mensagens não lidas", - "aria/{{ count }} unread message_other": "{{ count }} mensagens não lidas", - "aria/{{ setting }} disabled": "{{ setting }} desativado", - "aria/{{ setting }} enabled": "{{ setting }} ativado", - "aria/Active": "Ativo", - "aria/Animated GIF": "GIF animado", - "aria/Animated GIF: {{ title }}": "GIF animado: {{ title }}", - "aria/Attachment": "Anexo", - "aria/Attachment {{ attachmentType }}": "Anexo {{ attachmentType }}", - "aria/Attachment Actions": "Ações do anexo", - "aria/audio": "áudio", - "aria/Audio position {{ elapsed }} of {{ duration }}": "Posição do áudio {{ elapsed }} de {{ duration }}", - "aria/Audio position {{ progress }} percent": "Posição do áudio {{ progress }} por cento", - "aria/Back to attachments": "Voltar aos anexos", - "aria/Back to parent menu button": "Voltar ao menu principal botão", - "aria/Block User": "Bloquear usuário", - "aria/Bookmark Message": "Marcar mensagem", - "aria/Cancel recording": "Cancelar gravação", - "aria/Cancel Reply": "Cancelar resposta", - "aria/Channel Actions": "Ações do canal", - "aria/Channel details": "Detalhes do canal", - "aria/Channel list": "Lista de canais", - "aria/Chat view controls": "Controles da visualização do chat", - "aria/Chat: {{ channelName }}": "Chat: {{ channelName }}", - "aria/Clear search": "Limpar pesquisa", - "aria/Close callout dialog": "Fechar diálogo de destaque", - "aria/Close thread": "Fechar tópico", - "aria/Collapse sidebar": "Recolher barra lateral", - "aria/Command activated: {{ command }}": "Comando ativado: {{ command }}", - "aria/Command Suggestions": "Sugestões de comandos", - "aria/Complete recording": "Concluir gravação", - "aria/Copy Message Text": "Copiar texto da mensagem", - "aria/Decrease value": "Diminuir valor", - "aria/Delete Message": "Excluir mensagem", - "aria/Delivered": "Entregue", - "aria/Delivery status: {{ deliveryStatus }}": "Status de entrega: {{ deliveryStatus }}", - "aria/Dismiss notification": "Dispensar notificação", - "aria/Download attachment": "Baixar anexo", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "\"{{ option }}\" solto na posição {{ position }}.", - "aria/Edit Message": "Editar Mensagem", - "aria/Emoji picker": "Seletor de emojis", - "aria/Emoji Suggestions": "Sugestões de emojis", - "aria/Exit search": "Sair da pesquisa", - "aria/Expand sidebar": "Expandir barra lateral", - "aria/file": "arquivo", - "aria/File upload": "Carregar arquivo", - "aria/Flag Message": "Reportar mensagem", - "aria/GIF": "GIF", - "aria/Giphy actions": "Ações do Giphy", - "aria/Giphy canceled": "Giphy cancelado", - "aria/Giphy image changed": "Imagem do Giphy alterada", - "aria/Giphy image changed: {{ title }}": "Imagem do Giphy alterada: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Pré-visualização do Giphy, visível apenas para você. Use as ações Enviar, Embaralhar ou Cancelar.", - "aria/Giphy sent": "Giphy enviado", - "aria/Go back": "Voltar", - "aria/image": "imagem", - "aria/Image failed to load": "Falha ao carregar a imagem", - "aria/Increase value": "Aumentar valor", - "aria/Jump to latest message": "Ir para a mensagem mais recente", - "aria/Jump to quoted message": "Ir para a mensagem citada", - "aria/Last activity: {{ time }}": "Última atividade: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "Última mensagem de {{ sender }}: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "Última mensagem: {{ messagePreview }}", - "aria/Mark Message Unread": "Marcar como não lida", - "aria/Mark messages as read": "Marcar mensagens como lidas", - "aria/Mention Suggestions": "Sugestões de menções", - "aria/Message Actions": "Ações da mensagem", - "aria/Message from {{ user }},": "Mensagem de {{ user }},", - "aria/Message input": "Entrada de mensagem", - "aria/Message with attachments": "Mensagem com anexos", - "aria/Message,": "Mensagem,", - "aria/Mute User": "Silenciar usuário", - "aria/Next page": "Próxima página", - "aria/No search results found": "Nenhum resultado de pesquisa encontrado", - "aria/Notifications": "Notificações", - "aria/Open Attachment Selector": "Abrir seletor de anexos", - "aria/Open Channel Actions Menu": "Abrir menu de ações do canal", - "aria/Open channel details": "Abrir detalhes do canal", - "aria/Open channels view": "Abrir visualização de canais", - "aria/Open image shared by {{ name }}": "Abrir imagem compartilhada por {{ name }}", - "aria/Open Message Actions Menu": "Abrir menu de ações de mensagem", - "aria/Open Reaction Selector": "Abrir seletor de reações", - "aria/Open Thread": "Abrir tópico", - "aria/Open threads view": "Abrir visualização de tópicos", - "aria/Open threads view with unread threads_one": "Abrir visualização de tópicos, {{ count }} tópico não lido", - "aria/Open threads view with unread threads_many": "Abrir visualização de tópicos, {{ count }} tópicos não lidos", - "aria/Open threads view with unread threads_other": "Abrir visualização de tópicos, {{ count }} tópicos não lidos", - "aria/Open video shared by {{ name }}": "Abrir vídeo compartilhado por {{ name }}", - "aria/Opened channel: {{ name }}": "Canal aberto: {{ name }}", - "aria/Opened thread in {{ name }}": "Tópico aberto em {{ name }}", - "aria/Option {{ position }}": "Opção {{ position }}", - "aria/Options can now be reordered and removed.": "As opções agora podem ser reordenadas e removidas.", - "aria/Pause": "Pausar", - "aria/Pause recording": "Pausar gravação", - "aria/Percent complete": "{{percent}} por cento concluído", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "Você pegou \"{{ option }}\". Use as setas para reordenar. Pressione Espaço ou Tab para soltar.", - "aria/Pin Message": "Fixar mensagem", - "aria/Play": "Reproduzir", - "aria/Poll dialog opened": "Caixa de diálogo da enquete aberta", - "aria/Poll sent": "Enquete enviada", - "aria/Poll: {{ pollName }}": "Enquete: {{ pollName }}", - "aria/Press Enter to start typing": "Pressione Enter para começar a digitar", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "Pressione Espaço para selecionar esta opção, use as teclas de seta para Cima e para Baixo para movê-la e, em seguida, pressione Espaço novamente para desmarcá-la.", - "aria/Previous page": "Página anterior", - "aria/Quote Message": "Citar mensagem", - "aria/Reaction list": "Lista de reações", - "aria/Read": "Lida", - "aria/Recording paused": "Gravação pausada", - "aria/Recording resumed": "Gravação retomada", - "aria/Recording started": "Gravação iniciada", - "aria/Remind Me Message": "Lembrar-me", - "aria/Remove attachment": "Remover anexo", - "aria/Remove location attachment": "Remover anexo de localização", - "aria/Remove option: {{ option }}": "Remover opção: {{ option }}", - "aria/Remove Reminder": "Remover lembrete", - "aria/Remove Save For Later": "Remover Salvar para depois", - "aria/Removed option {{ option }}": "Opção {{ option }} removida", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "Reordenar \"{{ option }}\", posição {{ position }} de {{ total }}", - "aria/Reorder option {{ position }}": "Reordenar opção {{ position }}", - "aria/Resend Message": "Reenviar mensagem", - "aria/Resume recording": "Retomar gravação", - "aria/Retry upload": "Tentar upload novamente", - "aria/Review bounced message": "Revisar mensagem devolvida", - "aria/Search cleared": "Pesquisa limpa", - "aria/Search results": "Resultados da pesquisa", - "aria/Search results header filter button": "Botão de filtro do cabeçalho dos resultados da pesquisa", - "aria/Search results header filter button for: {{ source }}": "Botão de filtro do cabeçalho dos resultados da pesquisa para: {{ source }}", - "aria/Seek audio position": "Buscar posição do áudio", - "aria/Select Reaction: {{ reactionName }}": "Selecionar reação: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "Selecionar canal do usuário: {{ name }}", - "aria/Send": "Enviar", - "aria/Sent": "Enviada", - "aria/Shared a link": "Link compartilhado", - "aria/Shared a link with title: {{ linkTitle }}": "Link compartilhado com título: {{ linkTitle }}", - "aria/Shared location": "Localização compartilhada", - "aria/Show preview": "Mostrar prévia", - "aria/Start recording audio": "Iniciar gravação de áudio", - "aria/Stop AI Generation": "Parar geração de IA", - "aria/Submenu": "Submenu", - "aria/Suggestions": "Sugestões", - "aria/There are no messages in this chat.": "Não há mensagens neste chat", - "aria/This option can be reordered and removed.": "Esta opção pode ser reordenada e removida.", - "aria/Thread list": "Lista de tópicos", - "aria/Thread: {{ messagePreview }}": "Tópico: {{ messagePreview }}", - "aria/Unblock User": "Desbloquear usuário", - "aria/Unmute User": "Ativar som", - "aria/Unpin Message": "Desfixar mensagem", - "aria/User selected: {{ user }}": "Usuário selecionado: {{ user }}", - "aria/video": "vídeo", - "aria/voice message": "mensagem de voz", - "aria/Voice message sent": "Mensagem de voz enviada", - "aria/Voice recording attached": "Gravação de voz anexada", - "Ask a question": "Faça uma pergunta", - "Attach": "Anexar", - "Attach files": "Anexar arquivos", - "Attachment": "Anexo", - "Attachment upload blocked due to {{reason}}": "Upload de anexo bloqueado devido a {{reason}}", - "Attachment upload failed due to {{reason}}": "Upload de anexo falhou devido a {{reason}}", - "Back": "Voltar", - "ban-command-args": "[@nomedeusuário] [texto]", - "ban-command-description": "Banir um usuário", - "Block user": "Bloquear usuário", - "Block User": "Bloquear usuário", - "Browse channel members": "Ver membros do canal", - "Browse pinned messages": "Ver mensagens fixadas", - "Cancel": "Cancelar", - "Cannot seek in the recording": "Não é possível buscar na gravação", - "Changes saved": "Alterações salvas", - "Channel archived": "Canal arquivado", - "Channel members": "Membros do canal", - "Channel Missing": "Canal ausente", - "Channel muted": "Canal silenciado", - "Channel pinned": "Canal fixado", - "Channel unarchived": "Canal desarquivado", - "Channel unmuted": "Silêncio do canal desativado", - "Channel unpinned": "Canal desafixado", - "Channels": "Canais", - "Chat deleted": "Chat deleted", - "Chats": "Conversas", - "Choose between 2 to 10 options": "Escolha entre 2 a 10 opções", - "Close": "Fechar", - "Close dialog": "Fechar diálogo", - "Close emoji picker": "Fechar seletor de emoji", - "Command not available while editing": "Comando não disponível durante a edição", - "Command not available while replying": "Comando não disponível durante a resposta", - "Commands": "Comandos", - "Commands matching": "Comandos correspondentes", - "Connection failure, reconnecting now...": "Falha de conexão, reconectando agora...", - "Contact info": "Informações do contato", - "Contact name": "Nome do contato", - "Copy Message": "Copiar mensagem", - "Create": "Criar", - "Create a question, add options, and configure poll settings": "Crie uma pergunta, adicione opções e configure as definições da enquete", - "Create poll": "Criar enquete", - "Current location": "Localização atual", - "Delete": "Excluir", - "Delete chat": "Excluir chat", - "Delete for me": "Excluir para mim", - "Delete message": "Excluir mensagem", - "Delivered": "Entregue", - "Direct message": "Mensagem direta", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "Deseja encerrar esta enquete agora? Ninguém poderá mais votar nesta enquete.", - "Download {{ fileName }}": "Baixar {{ fileName }}", - "Download All": "Baixar tudo", - "Download Attachment": "Baixar anexo", - "Download attachment {{ name }}": "Baixar anexo {{ name }}", - "Download attachment {{ number }}": "Baixar anexo {{ number }}", - "Drag your files here": "Arraste seus arquivos aqui", - "Drag your files here to add to your post": "Arraste seus arquivos aqui para adicionar ao seu post", - "Due {{ timeLeft }}": "Vence em {{ timeLeft }}", - "Due since {{ dueSince }}": "Vencido desde {{ dueSince }}", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "Editar", - "Edit chat data": "Editar dados do chat", - "Edit contact": "Editar contato", - "Edit group": "Editar grupo", - "Edit Message": "Editar Mensagem", - "Edit message request failed": "O pedido de edição da mensagem falhou", - "Edited": "Editada", - "Emoji matching": "Emoji correspondente", - "Empty message...": "Mensagem vazia...", - "End": "Fim", - "End poll": "Encerrar enquete", - "End this poll?": "Encerrar esta enquete?", - "End vote": "Encerrar votação", - "Enforce unique vote is enabled": "Voto único está habilitado", - "Error": "Erro", - "Error · Unsent": "Erro · Não enviado", - "Error adding flag": "Erro ao reportar", - "Error adding members": "Error adding members", - "Error blocking user": "Erro ao bloquear usuário", - "Error connecting to chat, refresh the page to try again.": "Erro ao conectar ao bate-papo, atualize a página para tentar novamente.", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "Erro ao deletar mensagem", - "Error fetching reactions": "Erro ao carregar reações", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "Erro ao marcar a mensagem como não lida. Não é possível marcar mensagens não lidas mais antigas do que as 100 mensagens mais recentes do canal.", - "Error muting a user ...": "Erro ao silenciar um usuário...", - "Error muting channel": "Erro ao silenciar canal", - "Error muting user": "Erro ao silenciar usuário", - "Error opening direct message": "Erro ao abrir mensagem direta", - "Error pinning message": "Erro ao fixar mensagem", - "Error removing members": "Erro ao remover membros", - "Error removing message pin": "Erro ao remover o PIN da mensagem", - "Error removing user": "Erro ao remover usuário", - "Error reproducing the recording": "Erro ao reproduzir a gravação", - "Error starting recording": "Erro ao iniciar a gravação", - "Error unblocking user": "Erro ao desbloquear usuário", - "Error unmuting a user ...": "Erro ao ativar o som de um usuário...", - "Error unmuting channel": "Erro ao remover silenciamento do canal", - "Error unmuting user": "Erro ao remover silenciamento do usuário", - "Error uploading attachment": "Erro ao carregar o anexo", - "Error uploading file": "Erro ao enviar arquivo", - "Error uploading image": "Erro ao carregar a imagem", - "Error: {{ errorMessage }}": "Erro: {{ errorMessage }}", - "Exit command {{ command }}": "Sair do comando {{ command }}", - "Failed to block user": "Falha ao bloquear o usuário", - "Failed to create the poll": "Falha ao criar a pesquisa", - "Failed to create the poll due to {{reason}}": "Falha ao criar a enquete devido a {{reason}}", - "Failed to delete the message": "Falha ao excluir a mensagem", - "Failed to end the poll": "Falha ao encerrar a enquete", - "Failed to end the poll due to {{reason}}": "Falha ao encerrar a enquete devido a {{reason}}", - "Failed to jump to the first unread message": "Falha ao pular para a primeira mensagem não lida", - "Failed to leave channel": "Falha ao sair do canal", - "Failed to load channels": "Falha ao carregar os canais", - "Failed to load more channels": "Falha ao carregar mais canais", - "Failed to mark channel as read": "Falha ao marcar o canal como lido", - "Failed to play the recording": "Falha ao reproduzir a gravação", - "Failed to retrieve location": "Falha ao obter localização", - "Failed to save changes": "Falha ao salvar alterações", - "Failed to share location": "Falha ao compartilhar localização", - "Failed to update channel archive status": "Falha ao atualizar o status de arquivamento do canal", - "Failed to update channel mute status": "Falha ao atualizar o status de mudo do canal", - "Failed to update channel pinned status": "Falha ao atualizar o status de fixação do canal", - "File": "Arquivo", - "File is required for upload attachment": "Arquivo é necessário para enviar o anexo", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "O arquivo é muito grande: {{ size }}, o tamanho máximo de upload é {{ limit }}", - "File too large": "Arquivo muito grande", - "fileCount_one": "1 arquivo", - "fileCount_many": "{{ count }} arquivos", - "fileCount_other": "{{ count }} arquivos", - "Files": "Arquivos", - "Flag": "Reportar", - "Generating...": "Gerando...", - "giphy-command-args": "[texto]", - "giphy-command-description": "Postar um gif aleatório no canal", - "Go back": "Voltar", - "Group info": "Informações do grupo", - "Group name": "Nome do grupo", - "Hide who voted": "Ocultar quem votou", - "Image": "Imagem", - "imageCount_one": "Imagem", - "imageCount_many": "{{ count }} imagens", - "imageCount_other": "{{ count }} imagens", - "Instant commands": "Comandos instantâneos", - "language/af": "Africâner", - "language/am": "Amárico", - "language/ar": "Árabe", - "language/az": "Azerbaijano", - "language/bg": "Búlgaro", - "language/bn": "Bengali", - "language/bs": "Bósnio", - "language/cs": "Tcheco", - "language/da": "Dinamarquês", - "language/de": "Alemão", - "language/el": "Grego", - "language/en": "Inglês", - "language/es": "Espanhol", - "language/es-MX": "Espanhol (México)", - "language/et": "Estoniano", - "language/fa": "Persa", - "language/fa-AF": "Dari", - "language/fi": "Finlandês", - "language/fr": "Francês", - "language/fr-CA": "Francês (Canadá)", - "language/ha": "Hauçá", - "language/he": "Hebraico", - "language/hi": "Hindi", - "language/hr": "Croata", - "language/ht": "Crioulo haitiano", - "language/hu": "Húngaro", - "language/id": "Indonésio", - "language/it": "Italiano", - "language/ja": "Japonês", - "language/ka": "Georgiano", - "language/ko": "Coreano", - "language/lt": "Lituano", - "language/lv": "Letão", - "language/ms": "Malaio", - "language/nl": "Holandês", - "language/no": "Norueguês", - "language/pl": "Polonês", - "language/ps": "Pashto", - "language/pt": "Português", - "language/ro": "Romeno", - "language/ru": "Russo", - "language/sk": "Eslovaco", - "language/sl": "Esloveno", - "language/so": "Somali", - "language/sq": "Albanês", - "language/sr": "Sérvio", - "language/sv": "Sueco", - "language/sw": "Suaíli", - "language/ta": "Tâmil", - "language/th": "Tailandês", - "language/tl": "Tagalo", - "language/tr": "Turco", - "language/uk": "Ucraniano", - "language/ur": "Urdu", - "language/vi": "Vietnamita", - "language/zh": "Chinês (simplificado)", - "language/zh-TW": "Chinês (tradicional)", - "Last seen {{ timestamp }}": "Visto pela última vez {{ timestamp }}", - "Leave Channel": "Sair do canal", - "Leave chat": "Sair do canal", - "Left channel": "Canal abandonado", - "Let others add options": "Permitir que outros adicionem opções", - "Limit votes per person": "Limitar votos por pessoa", - "Link": "Link", - "linkCount_one": "Link", - "linkCount_many": "{{ count }} links", - "linkCount_other": "{{ count }} links", - "live": "ao vivo", - "Live for {{duration}}": "Ao vivo por {{duration}}", - "Live location": "Localização ao vivo", - "Live until {{ timestamp }}": "Ao vivo até {{ timestamp }}", - "Load more": "Carregar mais", - "Local upload attachment missing local id": "Anexo de envio local sem id local", - "Location": "Localização", - "Location sharing ended": "Compartilhamento de localização encerrado", - "Location: {{ coordinates }}": "Localização: {{ coordinates }}", - "Manage channel": "Gerenciar canal", - "Manage members": "Gerenciar membros", - "Mark as unread": "Marcar como não lida", - "Maximum number of votes (from 2 to 10)": "Número máximo de votos (de 2 a 10)", - "Maximum votes per person": "Máximo de votos por pessoa", - "Member detail": "Detalhes do membro", - "mention/Channel": "Canal", - "mention/Channel Description": "Notificar todos neste canal", - "mention/Here": "Aqui", - "mention/Here Description": "Notificar todos os membros online neste canal", - "Menu": "Menu", - "Message deleted": "Mensagem apagada", - "Message Failed · Click to try again": "A mensagem falhou · Clique para tentar novamente", - "Message Failed · Unauthorized": "A mensagem falhou · não autorizado", - "Message failed to send": "Falha ao enviar a mensagem", - "Message has been successfully flagged": "A mensagem foi reportada com sucesso", - "Message marked as unread": "Mensagem marcada como não lida", - "Message pinned": "Mensagem fixada", - "Message unpinned": "Mensagem desafixada", - "Message was blocked by moderation policies": "A mensagem foi bloqueada pelas políticas de moderação", - "Messages have been marked unread.": "Mensagens foram marcadas como não lidas.", - "Missing permissions to upload the attachment": "Faltando permissões para enviar o anexo", - "Moderator": "Moderador", - "Multiple votes": "Votos múltiplos", - "Mute": "Silenciar", - "Mute chat": "Silenciar chat", - "Mute user": "Silenciar usuário", - "mute-command-args": "[@nomedeusuário]", - "mute-command-description": "Silenciar um usuário", - "network error": "erro de rede", - "New": "Novo", - "New message from {{user}}": "Nova mensagem de {{user}}", - "New Messages!": "Novas Mensagens!", - "Next": "Próximo", - "Next image": "Próxima imagem", - "No chats here yet…": "Ainda não há conversas aqui...", - "No conversations yet": "Ainda não há conversas", - "No files": "Nenhum arquivo", - "No items exist": "Não existem itens", - "No member found": "Nenhum membro encontrado", - "No messages found": "Nenhuma mensagem encontrada", - "No photos or videos": "Nenhuma foto ou vídeo", - "No pinned messages": "Nenhuma mensagem fixada", - "No results found": "Nenhum resultado encontrado", - "No user found": "Nenhum usuário encontrado", - "Nobody will be able to vote in this poll anymore.": "Ninguém mais poderá votar nesta pesquisa.", - "Nothing yet...": "Nada ainda...", - "Notify all {{ role }} members": "Notificar todos os membros com a função {{ role }}", - "Offline": "Offline", - "Ok": "OK", - "Online": "Online", - "Only numbers are allowed": "Apenas números são permitidos", - "Only visible to you": "Visível apenas para você", - "Open emoji picker": "Abrir seletor de emoji", - "Open gallery at image {{ index }}": "Abrir galeria na imagem {{ index }}", - "Open image in gallery": "Abrir imagem na galeria", - "Open location in a map": "Abrir localização em um mapa", - "Open members actions": "Open members actions", - "Open menu": "Abrir menu", - "Option already exists": "Opção já existe", - "Option is empty": "A opção está vazia", - "Options": "Opções", - "Original": "Original", - "Owner": "Proprietário", - "People matching": "Pessoas correspondentes", - "Photo": "Foto", - "Photos & videos": "Fotos e vídeos", - "Pin": "Fixar", - "Pin a message to see it here": "Fixe uma mensagem para vê-la aqui", - "Pinned by {{ name }}": "Fixado por {{ name }}", - "Pinned by You": "Fixado por você", - "Pinned message": "Mensagem fixada", - "Pinned messages": "Mensagens fixadas", - "placeholder/PollComment": "O seu comentário", - "placeholder/PollOptionSuggestion": "Introduza uma nova opção", - "Play video": "Reproduzir vídeo", - "Playback speed {{ rate }}x": "Velocidade de reprodução {{ rate }}x", - "Poll": "Enquete", - "Poll comments": "Comentários da pesquisa", - "Poll ended": "Enquete encerrada", - "Poll options": "Opções da pesquisa", - "Poll results": "Resultados da pesquisa", - "Poll sent": "Enquete enviada", - "Previous": "Anterior", - "Previous image": "Imagem anterior", - "Question": "Pergunta", - "Question {{ optionOrderNumber}}": "Pergunta {{ optionOrderNumber}}", - "Question is required": "A pergunta é obrigatória", - "Quote Reply": "Responder com citação", - "Reached the vote limit. Remove an existing vote first.": "Limite de votos atingido. Remova um voto existente primeiro.", - "Recording format is not supported and cannot be reproduced": "Formato de gravação não é suportado e não pode ser reproduzido", - "Remind me": "Lembrar-me", - "Remind Me": "Lembrar-me", - "Reminder set": "Lembrete definido", - "Remove": "Remover", - "Remove {{ count }} members_one": "Remover {{ count }} membro", - "Remove {{ count }} members_many": "Remover {{ count }} membros", - "Remove {{ count }} members_other": "Remover {{ count }} membros", - "Remove {{ member }} from this channel?": "Remover {{ member }} deste canal?", - "Remove channel members": "Remover membros do canal", - "Remove reminder": "Remover lembrete", - "Remove save for later": "Remover Salvar para depois", - "Remove user": "Remover usuário", - "Removed {{ count }} members_one": "{{ count }} membro removido", - "Removed {{ count }} members_many": "{{ count }} membros removidos", - "Removed {{ count }} members_other": "{{ count }} membros removidos", - "Replied to a thread": "Respondeu em um tópico", - "Reply": "Responder", - "Reply to {{ authorName }}": "Responder a {{ authorName }}", - "Reply to a message to start a thread": "Responda a uma mensagem para iniciar um thread", - "Reply to Message": "Responder à mensagem", - "replyCount_one": "1 resposta", - "replyCount_many": "{{ count }} respostas", - "replyCount_other": "{{ count }} respostas", - "Resend": "Reenviar", - "Retry upload": "Tentar enviar novamente", - "Review all options available in this poll": "Revise todas as opções disponíveis nesta enquete", - "Review comments submitted with poll answers": "Revise comentários enviados com respostas da enquete", - "Review poll results and open an option to see detailed votes": "Revise os resultados da enquete e abra uma opção para ver votos detalhados", - "Review this message and choose whether to delete it, edit it, or send it anyway": "Revise esta mensagem e escolha se deseja excluí-la, editá-la ou enviá-la mesmo assim", - "Review who voted for this option": "Revise quem votou nesta opção", - "Save": "Salvar", - "Save for later": "Salvar para depois", - "Saved for later": "Salvo para depois", - "Search": "Buscar", - "Search GIFs": "Pesquisar GIFs", - "search-results-header-filter-source-button-label--channels": "canais", - "search-results-header-filter-source-button-label--messages": "mensagens", - "search-results-header-filter-source-button-label--users": "usuários", - "Searching for {{ searchSourceType }}...": "Buscando {{ searchSourceType }}...", - "Searching...": "Buscando...", - "searchResultsCount_one": "1 resultado", - "searchResultsCount_many": "{{ count }} resultados", - "searchResultsCount_other": "{{ count }} resultados", - "See all options ({{count}})_one": "Ver todas as opções ({{count}})", - "See all options ({{count}})_many": "Ver todas as opções ({{count}})", - "See all options ({{count}})_other": "Ver todas as opções ({{count}})", - "Select a thread to continue the conversation": "Selecione uma thread para continuar a conversa", - "Select more than one option": "Selecionar mais de uma opção", - "Select one": "Selecionar um", - "Select one or more": "Selecionar um ou mais", - "Select up to {{count}}_one": "Selecionar até {{count}}", - "Select up to {{count}}_many": "Selecionar até {{count}}", - "Select up to {{count}}_other": "Selecionar até {{count}}", - "Select your current location and optionally enable live location sharing": "Selecione sua localização atual e, opcionalmente, ative o compartilhamento de localização ao vivo", - "Send": "Enviar", - "Send a message": "Envie uma mensagem", - "Send a message to start the conversation": "Envie uma mensagem para iniciar a conversa", - "Send Anyway": "Enviar de qualquer forma", - "Send direct message": "Enviar mensagem direta", - "Send message request failed": "O pedido de envio da mensagem falhou", - "Send poll": "Enviar enquete", - "Sending...": "Enviando...", - "Sent": "Enviado", - "Share": "Compartilhar", - "Share a file to see it here": "Compartilhe um arquivo para vê-lo aqui", - "Share a photo or video to see it here": "Compartilhe uma foto ou vídeo para vê-lo aqui", - "Share live location for": "Compartilhar localização ao vivo por", - "Share Location": "Compartilhar localização", - "Shared live location": "Localização ao vivo compartilhada", - "Shared location": "Localização compartilhada", - "Show all": "Mostrar tudo", - "Shuffle": "Embaralhar", - "size limit": "limite de tamanho", - "Slow Mode ON": "Modo lento LIGADO", - "Slow mode, wait {{ seconds }}s...": "Modo lento, aguarde {{ seconds }} s...", - "Some of the files will not be accepted": "Alguns arquivos não serão aceitos", - "Start typing to search": "Comece a digitar para pesquisar", - "Stop sharing": "Parar de compartilhar", - "Submit": "Enviar", - "Suggest a new option to add to this poll": "Sugira uma nova opção para adicionar a esta enquete", - "Suggest an option": "Sugerir uma opção", - "Tap to remove": "Toque para remover", - "Tap to remove: {{ reactionName }}": "Toque para remover: {{ reactionName }}", - "Thinking...": "Pensando...", - "this content could not be displayed": "este conteúdo não pôde ser exibido", - "This field cannot be empty or contain only spaces": "Este campo não pode estar vazio ou conter apenas espaços", - "This message did not meet our content guidelines": "Esta mensagem não corresponde às nossas diretrizes de conteúdo", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "Este usuário poderá enviar mensagens para você novamente.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "Fio", - "Thread has not been found": "Fio não encontrado", - "Thread reply": "Resposta no fio", - "Thread Reply": "Resposta no fio", - "ThreadListUnseenThreadsBanner/loading": "Carregando...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} tópico não lido", - "ThreadListUnseenThreadsBanner/unreadThreads_many": "{{ count }} tópicos não lidos", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} tópicos não lidos", - "Threads": "Tópicos", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Ontem]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoje]\", \"nextDay\": \"[Amanhã]\", \"lastDay\": \"[Ontem]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Último] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "há {{ count }} d", - "timestamp/relativeToday": "Hoje", - "timestamp/relativeWeeksAgo": "há {{ count }} sem", - "timestamp/relativeYesterday": "Ontem", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Hoje] [às] HH:mm\", \"nextDay\": \"[Amanhã] [às] HH:mm\", \"lastDay\": \"[Ontem] [às] HH:mm\", \"nextWeek\": \"dddd [às] HH:mm\", \"lastWeek\": \"dddd [passada às] HH:mm\", \"sameElse\": \"ddd, D MMM [às] HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "Para começar a gravar, permita o acesso à câmera no seu navegador", - "To start recording, allow the microphone access in your browser": "Para começar a gravar, permita o acesso ao microfone no seu navegador", - "totalVoteCount_one": "1 voto no total", - "totalVoteCount_many": "{{ count }} votos no total", - "totalVoteCount_other": "{{ count }} votos no total", - "Translated": "Traduzido", - "Translated from {{ language }}": "Traduzido de {{ language }}", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "Digite um número de 2 a 10", - "Type your message": "Digite sua mensagem", - "Unarchive": "Desarquivar", - "unban-command-args": "[@nomedeusuário]", - "unban-command-description": "Desbanir um usuário", - "Unblock": "Desbloquear", - "Unblock user": "Desbloquear usuário", - "Unblock User": "Desbloquear usuário", - "unknown error": "erro desconhecido", - "Unmute": "Ativar som", - "Unmute chat": "Remover silenciamento do chat", - "Unmute user": "Remover silenciamento do usuário", - "unmute-command-args": "[@nomedeusuário]", - "unmute-command-description": "Retirar o silenciamento de um usuário", - "Unpin": "Desfixar", - "Unread messages": "Mensagens não lidas", - "Unsupported attachment": "Anexo não suportado", - "unsupported file type": "tipo de arquivo não suportado", - "Update": "Atualizar", - "Update the comment attached to your poll answer": "Atualize o comentário anexado à sua resposta da enquete", - "Update your comment": "Atualizar seu comentário", - "Upload blocked": "Envio bloqueado", - "Upload error": "Erro no envio", - "Upload failed": "Falha no envio", - "Upload Picture": "Enviar imagem", - "Upload type: \"{{ type }}\" is not allowed": "Tipo de upload: \"{{ type }}\" não é permitido", - "User blocked": "Usuário bloqueado", - "User muted": "Usuário silenciado", - "User removed": "Usuário removido", - "User unblocked": "Usuário desbloqueado", - "User unmuted": "Silenciamento do usuário removido", - "User uploaded content": "Conteúdo enviado pelo usuário", - "Video": "Vídeo", - "videoCount_one": "Vídeo", - "videoCount_many": "{{ count }} vídeos", - "videoCount_other": "{{ count }} vídeos", - "View": "Ver", - "View {{count}} comments_one": "Ver {{count}} comentário", - "View {{count}} comments_many": "Ver {{count}} comentários", - "View {{count}} comments_other": "Ver {{count}} comentários", - "View all": "Ver tudo", - "View member details for {{ member }}": "Ver detalhes do membro {{ member }}", - "View original": "Ver original", - "View results": "Ver resultados", - "View translation": "Ver tradução", - "Voice message": "Mensagem de voz", - "Voice message {{ duration }}": "Mensagem de voz {{ duration }}", - "Voice message deleted": "Mensagem de voz excluída", - "voiceMessageCount_one": "Mensagem de voz", - "voiceMessageCount_many": "{{ count }} mensagens de voz", - "voiceMessageCount_other": "{{ count }} mensagens de voz", - "Vote ended": "Votação encerrada", - "Votes": "Votos", - "Wait until all attachments have uploaded": "Espere até que todos os anexos tenham sido carregados", - "Waiting for network…": "Aguardando rede…", - "You": "Você", - "You have no channels currently": "Você não tem canais atualmente", - "You've reached the maximum number of files": "Você atingiu o número máximo de arquivos" -} diff --git a/src/i18n/ru.json b/src/i18n/ru.json deleted file mode 100644 index 6462c8b12f..0000000000 --- a/src/i18n/ru.json +++ /dev/null @@ -1,774 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} и {{ moreCount }} еще", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} и {{ lastUser }}", - "{{ count }} files_one": "{{ count }} файл", - "{{ count }} files_few": "{{ count }} файла", - "{{ count }} files_many": "{{ count }} файлов", - "{{ count }} files_other": "{{ count }} файла", - "{{ count }} members_one": "{{ count }} участник", - "{{ count }} members_few": "{{ count }} участника", - "{{ count }} members_many": "{{ count }} участников", - "{{ count }} members_other": "{{ count }} участника", - "{{ count }} members added_one": "{{ count }} участник добавлен", - "{{ count }} members added_few": "{{ count }} участника добавлены", - "{{ count }} members added_many": "{{ count }} участников добавлено", - "{{ count }} members added_other": "{{ count }} участника добавлены", - "{{ count }} people are typing_one": "{{ count }} человек печатает", - "{{ count }} people are typing_few": "{{ count }} человека печатают", - "{{ count }} people are typing_many": "{{ count }} человек печатают", - "{{ count }} people are typing_other": "{{ count }} человека печатают", - "{{ count }} photos_one": "{{ count }} фото", - "{{ count }} photos_few": "{{ count }} фото", - "{{ count }} photos_many": "{{ count }} фото", - "{{ count }} photos_other": "{{ count }} фото", - "{{ count }} reactions_one": "{{ count }} реакция", - "{{ count }} reactions_few": "{{ count }} реакции", - "{{ count }} reactions_many": "{{ count }} реакций", - "{{ count }} reactions_other": "{{ count }} реакций", - "{{ count }} videos_one": "{{ count }} видео", - "{{ count }} videos_few": "{{ count }} видео", - "{{ count }} videos_many": "{{ count }} видео", - "{{ count }} videos_other": "{{ count }} видео", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} и {{ secondUser }}", - "{{ imageCount }} more": "Ещё {{ imageCount }}", - "{{ member }} will be able to message you again.": "{{ member }} снова сможет отправлять вам сообщения.", - "{{ member }} won't be able to message you anymore.": "{{ member }} больше не сможет отправлять вам сообщения.", - "{{ memberCount }} members": "{{ memberCount }} участников", - "{{ typing }} are typing": "{{ typing }} печатают", - "{{ typing }} is typing": "{{ typing }} печатает", - "{{ user }} has been muted": "Вы отписались от уведомлений от {{ user }}", - "{{ user }} has been unmuted": "Уведомления от {{ user }} были включены", - "{{ user }} is typing...": "{{ user }} печатает...", - "{{ users }} and {{ user }} are typing...": "{{ users }} и {{ user }} печатают...", - "{{ users }} and more are typing...": "{{ users }} и другие печатают...", - "{{ watcherCount }} online": "{{ watcherCount }} в сети", - "{{count}} new messages_one": "{{count}} новое сообщение", - "{{count}} new messages_few": "{{count}} новых сообщения", - "{{count}} new messages_many": "{{count}} новых сообщений", - "{{count}} new messages_other": "{{count}} новых сообщений", - "{{count}} unread_one": "{{count}} непрочитанное", - "{{count}} unread_few": "{{count}} непрочитанных", - "{{count}} unread_many": "{{count}} непрочитанных", - "{{count}} unread_other": "{{count}} непрочитанных", - "{{count}} votes_one": "{{count}} голос", - "{{count}} votes_few": "{{count}} голоса", - "{{count}} votes_many": "{{count}} голосов", - "{{count}} votes_other": "{{count}} голосов", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+ещё {{count}} вариант", - "+{{count}} more options_few": "+ещё {{count}} варианта", - "+{{count}} more options_many": "+ещё {{count}} вариантов", - "+{{count}} more options_other": "+ещё {{count}} вариантов", - "🏙 Attachment...": "🏙 Вложение...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} создал(а): {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} проголосовал(а): {{pollOptionText}}", - "📍Shared location": "📍Общее местоположение", - "Actions": "Actions", - "Add": "Добавить", - "Add {{ count }} members_one": "Добавить {{ count }} участника", - "Add {{ count }} members_few": "Добавить {{ count }} участников", - "Add {{ count }} members_many": "Добавить {{ count }} участников", - "Add {{ count }} members_other": "Добавить {{ count }} участника", - "Add a comment": "Добавить комментарий", - "Add a comment to your poll answer": "Добавьте комментарий к вашему ответу в опросе", - "Add an option": "Добавить вариант", - "Add channel members": "Добавить участников канала", - "Add members": "Добавить участников", - "Add reaction": "Добавить реакцию", - "Admin": "Администратор", - "All results loaded": "Все результаты загружены", - "Allow access to camera": "Разрешить доступ к камере", - "Allow access to microphone": "Разрешить доступ к микрофону", - "Allow comments": "Разрешить комментарии", - "Allow option suggestion": "Разрешить предложение вариантов", - "Allow others to add comments": "Разрешить другим добавлять комментарии", - "Already a member": "Уже участник", - "Also send as a direct message": "Также отправить как личное сообщение", - "Also send in channel": "Также отправить в канал", - "Also sent in channel": "Также отправлено в канал", - "An error has occurred during recording": "Произошла ошибка во время записи", - "An error has occurred during the recording processing": "Произошла ошибка во время обработки записи", - "Anonymous": "Аноним", - "Anonymous poll": "Анонимный опрос", - "Archive": "Aрхивировать", - "Are you sure you want to delete this message?": "Вы уверены, что хотите удалить это сообщение?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_few": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_many": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} вложение", - "aria/{{ count }} attachment_few": "{{ count }} вложения", - "aria/{{ count }} attachment_many": "{{ count }} вложений", - "aria/{{ count }} attachment_other": "{{ count }} вложений", - "aria/{{ count }} search results_one": "{{ count }} результат поиска", - "aria/{{ count }} search results_few": "{{ count }} результата поиска", - "aria/{{ count }} search results_many": "{{ count }} результатов поиска", - "aria/{{ count }} search results_other": "{{ count }} результата поиска", - "aria/{{ count }} suggestions_one": "{{ count }} подсказка", - "aria/{{ count }} suggestions_few": "{{ count }} подсказки", - "aria/{{ count }} suggestions_many": "{{ count }} подсказок", - "aria/{{ count }} suggestions_other": "{{ count }} подсказки", - "aria/{{ count }} unread message_one": "{{ count }} непрочитанное сообщение", - "aria/{{ count }} unread message_few": "{{ count }} непрочитанных сообщения", - "aria/{{ count }} unread message_many": "{{ count }} непрочитанных сообщений", - "aria/{{ count }} unread message_other": "{{ count }} непрочитанных сообщений", - "aria/{{ setting }} disabled": "{{ setting }} отключено", - "aria/{{ setting }} enabled": "{{ setting }} включено", - "aria/Active": "Активно", - "aria/Animated GIF": "Анимированный GIF", - "aria/Animated GIF: {{ title }}": "Анимированный GIF: {{ title }}", - "aria/Attachment": "Вложение", - "aria/Attachment {{ attachmentType }}": "Вложение {{ attachmentType }}", - "aria/Attachment Actions": "Действия с вложением", - "aria/audio": "аудио", - "aria/Audio position {{ elapsed }} of {{ duration }}": "Позиция аудио {{ elapsed }} из {{ duration }}", - "aria/Audio position {{ progress }} percent": "Позиция аудио {{ progress }} процентов", - "aria/Back to attachments": "Назад к вложениям", - "aria/Back to parent menu button": "Назад в родительское меню кнопка", - "aria/Block User": "Заблокировать пользователя", - "aria/Bookmark Message": "Сохранить сообщение", - "aria/Cancel recording": "Отменить запись", - "aria/Cancel Reply": "Отменить ответ", - "aria/Channel Actions": "Действия канала", - "aria/Channel details": "Сведения о канале", - "aria/Channel list": "Список каналов", - "aria/Chat view controls": "Элементы управления видом чата", - "aria/Chat: {{ channelName }}": "Чат: {{ channelName }}", - "aria/Clear search": "Очистить поиск", - "aria/Close callout dialog": "Закрыть диалог выноски", - "aria/Close thread": "Закрыть тему", - "aria/Collapse sidebar": "Свернуть боковую панель", - "aria/Command activated: {{ command }}": "Команда активирована: {{ command }}", - "aria/Command Suggestions": "Подсказки команд", - "aria/Complete recording": "Завершить запись", - "aria/Copy Message Text": "Копировать текст сообщения", - "aria/Decrease value": "Уменьшить значение", - "aria/Delete Message": "Удалить сообщение", - "aria/Delivered": "Доставлено", - "aria/Delivery status: {{ deliveryStatus }}": "Статус доставки: {{ deliveryStatus }}", - "aria/Dismiss notification": "Закрыть уведомление", - "aria/Download attachment": "Скачать вложение", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "Вариант «{{ option }}» помещён на позицию {{ position }}.", - "aria/Edit Message": "Редактировать сообщение", - "aria/Emoji picker": "Выбор эмодзи", - "aria/Emoji Suggestions": "Подсказки эмодзи", - "aria/Exit search": "Выйти из поиска", - "aria/Expand sidebar": "Развернуть боковую панель", - "aria/file": "файл", - "aria/File upload": "Загрузка файла", - "aria/Flag Message": "Пожаловаться на сообщение", - "aria/GIF": "GIF", - "aria/Giphy actions": "Действия Giphy", - "aria/Giphy canceled": "Giphy отменён", - "aria/Giphy image changed": "Giphy-изображение изменено", - "aria/Giphy image changed: {{ title }}": "Giphy-изображение изменено: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Предпросмотр Giphy, виден только вам. Используйте действия «Отправить», «Перемешать» или «Отменить».", - "aria/Giphy sent": "Giphy отправлен", - "aria/Go back": "Назад", - "aria/image": "изображение", - "aria/Image failed to load": "Не удалось загрузить изображение", - "aria/Increase value": "Увеличить значение", - "aria/Jump to latest message": "Перейти к последнему сообщению", - "aria/Jump to quoted message": "Перейти к цитируемому сообщению", - "aria/Last activity: {{ time }}": "Последняя активность: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "Последнее сообщение от {{ sender }}: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "Последнее сообщение: {{ messagePreview }}", - "aria/Mark Message Unread": "Отметить как непрочитанное", - "aria/Mark messages as read": "Отметить сообщения как прочитанные", - "aria/Mention Suggestions": "Подсказки упоминаний", - "aria/Message Actions": "Действия с сообщением", - "aria/Message from {{ user }},": "Сообщение от {{ user }},", - "aria/Message input": "Поле ввода сообщения", - "aria/Message with attachments": "Сообщение с вложениями", - "aria/Message,": "Сообщение,", - "aria/Mute User": "Отключить уведомления", - "aria/Next page": "Следующая страница", - "aria/No search results found": "Результаты поиска не найдены", - "aria/Notifications": "Уведомления", - "aria/Open Attachment Selector": "Открыть выбор вложений", - "aria/Open Channel Actions Menu": "Открыть меню действий канала", - "aria/Open channel details": "Открыть сведения о канале", - "aria/Open channels view": "Открыть вид каналов", - "aria/Open image shared by {{ name }}": "Открыть изображение от {{ name }}", - "aria/Open Message Actions Menu": "Открыть меню действий с сообщениями", - "aria/Open Reaction Selector": "Открыть селектор реакций", - "aria/Open Thread": "Открыть тему", - "aria/Open threads view": "Открыть вид веток", - "aria/Open threads view with unread threads_one": "Открыть вид веток, {{ count }} непрочитанная ветка", - "aria/Open threads view with unread threads_few": "Открыть вид веток, {{ count }} непрочитанные ветки", - "aria/Open threads view with unread threads_many": "Открыть вид веток, {{ count }} непрочитанных веток", - "aria/Open threads view with unread threads_other": "Открыть вид веток, {{ count }} непрочитанных веток", - "aria/Open video shared by {{ name }}": "Открыть видео от {{ name }}", - "aria/Opened channel: {{ name }}": "Открыт канал: {{ name }}", - "aria/Opened thread in {{ name }}": "Открыт тред в {{ name }}", - "aria/Option {{ position }}": "Вариант {{ position }}", - "aria/Options can now be reordered and removed.": "Теперь варианты можно переупорядочивать и удалять.", - "aria/Pause": "Пауза", - "aria/Pause recording": "Приостановить запись", - "aria/Percent complete": "{{percent}} процентов завершено", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "Вариант «{{ option }}» взят. Используйте клавиши со стрелками, чтобы изменить порядок. Нажмите пробел или Tab, чтобы отпустить.", - "aria/Pin Message": "Закрепить сообщение", - "aria/Play": "Воспроизвести", - "aria/Poll dialog opened": "Диалоговое окно опроса открыто", - "aria/Poll sent": "Опрос отправлен", - "aria/Poll: {{ pollName }}": "Опрос: {{ pollName }}", - "aria/Press Enter to start typing": "Нажмите Enter, чтобы начать ввод", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "Нажмите пробел, чтобы выбрать этот вариант, используйте клавиши со стрелками вверх и вниз, чтобы переместить его, затем снова нажмите пробел, чтобы снять выбор.", - "aria/Previous page": "Предыдущая страница", - "aria/Quote Message": "Цитировать сообщение", - "aria/Reaction list": "Список реакций", - "aria/Read": "Прочитано", - "aria/Recording paused": "Запись приостановлена", - "aria/Recording resumed": "Запись возобновлена", - "aria/Recording started": "Запись начата", - "aria/Remind Me Message": "Напомнить мне", - "aria/Remove attachment": "Удалить вложение", - "aria/Remove location attachment": "Удалить вложение местоположения", - "aria/Remove option: {{ option }}": "Удалить вариант: {{ option }}", - "aria/Remove Reminder": "Удалить напоминание", - "aria/Remove Save For Later": "Удалить «Сохранить на потом»", - "aria/Removed option {{ option }}": "Вариант {{ option }} удалён", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "Изменить порядок варианта «{{ option }}», позиция {{ position }} из {{ total }}", - "aria/Reorder option {{ position }}": "Изменить порядок варианта {{ position }}", - "aria/Resend Message": "Отправить сообщение повторно", - "aria/Resume recording": "Возобновить запись", - "aria/Retry upload": "Повторить загрузку", - "aria/Review bounced message": "Проверить отклонённое сообщение", - "aria/Search cleared": "Поиск очищен", - "aria/Search results": "Результаты поиска", - "aria/Search results header filter button": "Кнопка фильтра заголовка результатов поиска", - "aria/Search results header filter button for: {{ source }}": "Кнопка фильтра заголовка результатов поиска для: {{ source }}", - "aria/Seek audio position": "Перемотать аудио", - "aria/Select Reaction: {{ reactionName }}": "Выбрать реакцию: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "Выбрать пользовательский канал: {{ name }}", - "aria/Send": "Отправить", - "aria/Sent": "Отправлено", - "aria/Shared a link": "Поделились ссылкой", - "aria/Shared a link with title: {{ linkTitle }}": "Поделились ссылкой с заголовком: {{ linkTitle }}", - "aria/Shared location": "Местоположение", - "aria/Show preview": "Показать предпросмотр", - "aria/Start recording audio": "Начать запись аудио", - "aria/Stop AI Generation": "Остановить генерацию ИИ", - "aria/Submenu": "Подменю", - "aria/Suggestions": "Подсказки", - "aria/There are no messages in this chat.": "В этом чате нет сообщений", - "aria/This option can be reordered and removed.": "Этот вариант можно переупорядочить и удалить.", - "aria/Thread list": "Список тредов", - "aria/Thread: {{ messagePreview }}": "Тема: {{ messagePreview }}", - "aria/Unblock User": "Разблокировать пользователя", - "aria/Unmute User": "Включить уведомления", - "aria/Unpin Message": "Открепить сообщение", - "aria/User selected: {{ user }}": "Выбран пользователь: {{ user }}", - "aria/video": "видео", - "aria/voice message": "голосовое сообщение", - "aria/Voice message sent": "Голосовое сообщение отправлено", - "aria/Voice recording attached": "Голосовая запись прикреплена", - "Ask a question": "Задать вопрос", - "Attach": "Прикрепить", - "Attach files": "Прикрепить файлы", - "Attachment": "Вложение", - "Attachment upload blocked due to {{reason}}": "Загрузка вложения заблокирована из-за {{reason}}", - "Attachment upload failed due to {{reason}}": "Загрузка вложения не удалась из-за {{reason}}", - "Back": "Назад", - "ban-command-args": "[@имяпользователя] [текст]", - "ban-command-description": "Заблокировать пользователя", - "Block user": "Заблокировать пользователя", - "Block User": "Заблокировать пользователя", - "Browse channel members": "Просмотреть участников канала", - "Browse pinned messages": "Просмотреть закрепленные сообщения", - "Cancel": "Отмена", - "Cannot seek in the recording": "Невозможно осуществить поиск в записи", - "Changes saved": "Изменения сохранены", - "Channel archived": "Канал в архиве", - "Channel members": "Участники канала", - "Channel Missing": "Канал не найден", - "Channel muted": "Канал заглушён", - "Channel pinned": "Канал закреплён", - "Channel unarchived": "Канал извлечён из архива", - "Channel unmuted": "Заглушение канала снято", - "Channel unpinned": "Канал откреплён", - "Channels": "Каналы", - "Chat deleted": "Chat deleted", - "Chats": "Чаты", - "Choose between 2 to 10 options": "Выберите от 2 до 10 вариантов", - "Close": "Закрыть", - "Close dialog": "Закрыть диалог", - "Close emoji picker": "Закрыть окно выбора смайлов", - "Command not available while editing": "Команда недоступна при редактировании", - "Command not available while replying": "Команда недоступна при ответе", - "Commands": "Команды", - "Commands matching": "Соответствие команд", - "Connection failure, reconnecting now...": "Ошибка соединения, переподключение...", - "Contact info": "Информация о контакте", - "Contact name": "Имя контакта", - "Copy Message": "Копировать сообщение", - "Create": "Создать", - "Create a question, add options, and configure poll settings": "Создайте вопрос, добавьте варианты и настройте параметры опроса", - "Create poll": "Создать опрос", - "Current location": "Текущее местоположение", - "Delete": "Удалить", - "Delete chat": "Удалить чат", - "Delete for me": "Удалить для меня", - "Delete message": "Удалить сообщение", - "Delivered": "Отправлено", - "Direct message": "Личное сообщение", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "Вы хотите завершить этот опрос сейчас? После этого никто не сможет голосовать в этом опросе.", - "Download {{ fileName }}": "Скачать {{ fileName }}", - "Download All": "Скачать всё", - "Download Attachment": "Скачать вложение", - "Download attachment {{ name }}": "Скачать вложение {{ name }}", - "Download attachment {{ number }}": "Скачать вложение {{ number }}", - "Drag your files here": "Перетащите ваши файлы сюда", - "Drag your files here to add to your post": "Перетащите ваши файлы сюда, чтобы добавить их в ваш пост", - "Due {{ timeLeft }}": "Просрочено в {{ timeLeft }}", - "Due since {{ dueSince }}": "Просрочено с {{ dueSince }}", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "Редактировать", - "Edit chat data": "Редактировать данные чата", - "Edit contact": "Редактировать контакт", - "Edit group": "Редактировать группу", - "Edit Message": "Редактировать сообщение", - "Edit message request failed": "Не удалось изменить запрос сообщения", - "Edited": "Отредактировано", - "Emoji matching": "Соответствие эмодзи", - "Empty message...": "Пустое сообщение...", - "End": "Конец", - "End poll": "Завершить опрос", - "End this poll?": "Завершить этот опрос?", - "End vote": "Закончить голосование", - "Enforce unique vote is enabled": "Уникальное голосование включено", - "Error": "Ошибка", - "Error · Unsent": "Ошибка · Не отправлено", - "Error adding flag": "Ошибка добавления флага", - "Error adding members": "Error adding members", - "Error blocking user": "Ошибка при блокировке пользователя", - "Error connecting to chat, refresh the page to try again.": "Ошибка подключения к чату, обновите страницу чтобы попробовать снова.", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "Ошибка при удалении сообщения", - "Error fetching reactions": "Ошибка при загрузке реакций", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "Ошибка при отметке сообщения как непрочитанного. Невозможно отметить как непрочитанные сообщения старше последних 100 сообщений в канале.", - "Error muting a user ...": "Ошибка отключения уведомлений от пользователя...", - "Error muting channel": "Ошибка при отключении уведомлений канала", - "Error muting user": "Ошибка при отключении уведомлений пользователя", - "Error opening direct message": "Ошибка при открытии личного сообщения", - "Error pinning message": "Сообщение об ошибке при закреплении", - "Error removing members": "Ошибка при удалении участников", - "Error removing message pin": "Ошибка при удалении булавки сообщения", - "Error removing user": "Ошибка при удалении пользователя", - "Error reproducing the recording": "Ошибка воспроизведения записи", - "Error starting recording": "Ошибка при запуске записи", - "Error unblocking user": "Ошибка при разблокировке пользователя", - "Error unmuting a user ...": "Ошибка включения уведомлений...", - "Error unmuting channel": "Ошибка при включении уведомлений канала", - "Error unmuting user": "Ошибка при включении уведомлений пользователя", - "Error uploading attachment": "Ошибка при загрузке вложения", - "Error uploading file": "Ошибка при загрузке файла", - "Error uploading image": "Ошибка загрузки изображения", - "Error: {{ errorMessage }}": "Ошибка: {{ errorMessage }}", - "Exit command {{ command }}": "Выйти из команды {{ command }}", - "Failed to block user": "Не удалось заблокировать пользователя", - "Failed to create the poll": "Не удалось создать опрос", - "Failed to create the poll due to {{reason}}": "Не удалось создать опрос из-за {{reason}}", - "Failed to delete the message": "Не удалось удалить сообщение", - "Failed to end the poll": "Не удалось завершить опрос", - "Failed to end the poll due to {{reason}}": "Не удалось завершить опрос из-за {{reason}}", - "Failed to jump to the first unread message": "Не удалось перейти к первому непрочитанному сообщению", - "Failed to leave channel": "Не удалось покинуть канал", - "Failed to load channels": "Не удалось загрузить каналы", - "Failed to load more channels": "Не удалось загрузить больше каналов", - "Failed to mark channel as read": "Не удалось пометить канал как прочитанный", - "Failed to play the recording": "Не удалось воспроизвести запись", - "Failed to retrieve location": "Не удалось получить местоположение", - "Failed to save changes": "Не удалось сохранить изменения", - "Failed to share location": "Не удалось поделиться местоположением", - "Failed to update channel archive status": "Не удалось обновить статус архивации канала", - "Failed to update channel mute status": "Не удалось обновить статус отключения звука канала", - "Failed to update channel pinned status": "Не удалось обновить статус закрепления канала", - "File": "Файл", - "File is required for upload attachment": "Для загрузки вложения требуется файл", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "Файл слишком большой: {{ size }}, максимальный размер загрузки составляет {{ limit }}", - "File too large": "Файл слишком большой", - "fileCount_one": "{{ count }} файл", - "fileCount_two": "{{ count }} файла", - "fileCount_few": "{{ count }} файла", - "fileCount_four": "{{ count }} файла", - "fileCount_many": "{{ count }} файлов", - "fileCount_other": "{{ count }} файлов", - "fileCount_three": "{{ count }} файла", - "Files": "Файлы", - "Flag": "Пожаловаться", - "Generating...": "Генерирую...", - "giphy-command-args": "[текст]", - "giphy-command-description": "Опубликовать случайную GIF-анимацию в канале", - "Go back": "Назад", - "Group info": "Информация о группе", - "Group name": "Название группы", - "Hide who voted": "Скрыть, кто голосовал", - "Image": "Изображение", - "imageCount_one": "{{ count }} изображение", - "imageCount_few": "{{ count }} изображения", - "imageCount_many": "{{ count }} изображений", - "imageCount_other": "{{ count }} изображений", - "Instant commands": "Мгновенные команды", - "language/af": "Африкаанс", - "language/am": "Амхарский", - "language/ar": "Арабский", - "language/az": "Азербайджанский", - "language/bg": "Болгарский", - "language/bn": "Бенгальский", - "language/bs": "Боснийский", - "language/cs": "Чешский", - "language/da": "Датский", - "language/de": "Немецкий", - "language/el": "Греческий", - "language/en": "Английский", - "language/es": "Испанский", - "language/es-MX": "Испанский (Мексика)", - "language/et": "Эстонский", - "language/fa": "Персидский", - "language/fa-AF": "Дари", - "language/fi": "Финский", - "language/fr": "Французский", - "language/fr-CA": "Французский (Канада)", - "language/ha": "Хауса", - "language/he": "Иврит", - "language/hi": "Хинди", - "language/hr": "Хорватский", - "language/ht": "Гаитянский креольский", - "language/hu": "Венгерский", - "language/id": "Индонезийский", - "language/it": "Итальянский", - "language/ja": "Японский", - "language/ka": "Грузинский", - "language/ko": "Корейский", - "language/lt": "Литовский", - "language/lv": "Латышский", - "language/ms": "Малайский", - "language/nl": "Нидерландский", - "language/no": "Норвежский", - "language/pl": "Польский", - "language/ps": "Пушту", - "language/pt": "Португальский", - "language/ro": "Румынский", - "language/ru": "Русский", - "language/sk": "Словацкий", - "language/sl": "Словенский", - "language/so": "Сомали", - "language/sq": "Албанский", - "language/sr": "Сербский", - "language/sv": "Шведский", - "language/sw": "Суахили", - "language/ta": "Тамильский", - "language/th": "Тайский", - "language/tl": "Тагальский", - "language/tr": "Турецкий", - "language/uk": "Украинский", - "language/ur": "Урду", - "language/vi": "Вьетнамский", - "language/zh": "Китайский (упрощённый)", - "language/zh-TW": "Китайский (традиционный)", - "Last seen {{ timestamp }}": "Был(а) в сети {{ timestamp }}", - "Leave Channel": "Покинуть канал", - "Leave chat": "Покинуть канал", - "Left channel": "Канал покинут", - "Let others add options": "Разрешить другим добавлять варианты", - "Limit votes per person": "Ограничить голоса на человека", - "Link": "Линк", - "linkCount_one": "{{ count }} линк", - "linkCount_few": "{{ count }} линка", - "linkCount_many": "{{ count }} линков", - "linkCount_other": "{{ count }} линков", - "live": "В прямом эфире", - "Live for {{duration}}": "В прямом эфире {{duration}}", - "Live location": "Местоположение в прямом эфире", - "Live until {{ timestamp }}": "В прямом эфире до {{ timestamp }}", - "Load more": "Загрузить больше", - "Local upload attachment missing local id": "У локального вложения нет локального id", - "Location": "Местоположение", - "Location sharing ended": "Обмен местоположением завершен", - "Location: {{ coordinates }}": "Местоположение: {{ coordinates }}", - "Manage channel": "Управлять каналом", - "Manage members": "Управлять участниками", - "Mark as unread": "Отметить как непрочитанное", - "Maximum number of votes (from 2 to 10)": "Максимальное количество голосов (от 2 до 10)", - "Maximum votes per person": "Максимум голосов на человека", - "Member detail": "Сведения об участнике", - "mention/Channel": "Канал", - "mention/Channel Description": "Уведомить всех в этом канале", - "mention/Here": "Здесь", - "mention/Here Description": "Уведомить всех онлайн-участников в этом канале", - "Menu": "Меню", - "Message deleted": "Сообщение удалено", - "Message Failed · Click to try again": "Ошибка отправки сообщения · Нажмите чтобы повторить", - "Message Failed · Unauthorized": "Ошибка отправки сообщения · Неавторизованный", - "Message failed to send": "Не удалось отправить сообщение", - "Message has been successfully flagged": "Жалоба на сообщение была принята", - "Message marked as unread": "Сообщение помечено как непрочитанное", - "Message pinned": "Сообщение закреплено", - "Message unpinned": "Сообщение откреплено", - "Message was blocked by moderation policies": "Сообщение было заблокировано модерацией", - "Messages have been marked unread.": "Сообщения были отмечены как непрочитанные.", - "Missing permissions to upload the attachment": "Отсутствуют разрешения для загрузки вложения", - "Moderator": "Модератор", - "Multiple votes": "Несколько голосов", - "Mute": "Отключить уведомления", - "Mute chat": "Отключить уведомления чата", - "Mute user": "Отключить уведомления пользователя", - "mute-command-args": "[@имяпользователя]", - "mute-command-description": "Выключить микрофон у пользователя", - "network error": "ошибка сети", - "New": "Новые", - "New message from {{user}}": "Новое сообщение от {{user}}", - "New Messages!": "Новые сообщения!", - "Next": "Далее", - "Next image": "Следующее изображение", - "No chats here yet…": "Здесь еще нет чатов...", - "No conversations yet": "Пока нет бесед", - "No files": "Нет файлов", - "No items exist": "Элементов нет", - "No member found": "Участник не найден", - "No messages found": "Сообщения не найдены", - "No photos or videos": "Нет фото или видео", - "No pinned messages": "Нет закрепленных сообщений", - "No results found": "Результаты не найдены", - "No user found": "Пользователь не найден", - "Nobody will be able to vote in this poll anymore.": "Никто больше не сможет голосовать в этом опросе.", - "Nothing yet...": "Пока ничего нет...", - "Notify all {{ role }} members": "Уведомить всех участников с ролью {{ role }}", - "Offline": "Не в сети", - "Ok": "Ок", - "Online": "В сети", - "Only numbers are allowed": "Разрешены только цифры", - "Only visible to you": "Видно только вам", - "Open emoji picker": "Открыть выбор смайлов", - "Open gallery at image {{ index }}": "Открыть галерею на изображении {{ index }}", - "Open image in gallery": "Открыть изображение в галерее", - "Open location in a map": "Открыть местоположение на карте", - "Open members actions": "Open members actions", - "Open menu": "Открыть меню", - "Option already exists": "Вариант уже существует", - "Option is empty": "Вариант пуст", - "Options": "Варианты", - "Original": "Оригинал", - "Owner": "Владелец", - "People matching": "Совпадающие люди", - "Photo": "Фото", - "Photos & videos": "Фото и видео", - "Pin": "Закрепить", - "Pin a message to see it here": "Закрепите сообщение, чтобы увидеть его здесь", - "Pinned by {{ name }}": "Закреплено: {{ name }}", - "Pinned by You": "Закреплено вами", - "Pinned message": "Закрепленное сообщение", - "Pinned messages": "Закрепленные сообщения", - "placeholder/PollComment": "Ваш комментарий", - "placeholder/PollOptionSuggestion": "Введите новый вариант", - "Play video": "Воспроизвести видео", - "Playback speed {{ rate }}x": "Скорость воспроизведения {{ rate }}x", - "Poll": "Опрос", - "Poll comments": "Комментарии к опросу", - "Poll ended": "Опрос завершён", - "Poll options": "Опции опроса", - "Poll results": "Результаты опроса", - "Poll sent": "Опрос отправлен", - "Previous": "Назад", - "Previous image": "Предыдущее изображение", - "Question": "Вопрос", - "Question {{ optionOrderNumber}}": "Вопрос {{ optionOrderNumber}}", - "Question is required": "Вопрос обязателен", - "Quote Reply": "Ответ со цитатой", - "Reached the vote limit. Remove an existing vote first.": "Достигнут лимит голосов. Сначала удалите существующий голос.", - "Recording format is not supported and cannot be reproduced": "Формат записи не поддерживается и не может быть воспроизведен", - "Remind me": "Напомнить мне", - "Remind Me": "Напомнить мне", - "Reminder set": "Напоминание установлено", - "Remove": "Удалить", - "Remove {{ count }} members_one": "Удалить {{ count }} участника", - "Remove {{ count }} members_few": "Удалить {{ count }} участника", - "Remove {{ count }} members_many": "Удалить {{ count }} участников", - "Remove {{ count }} members_other": "Удалить {{ count }} участника", - "Remove {{ member }} from this channel?": "Удалить {{ member }} из этого канала?", - "Remove channel members": "Удалить участников канала", - "Remove reminder": "Удалить напоминание", - "Remove save for later": "Удалить «Сохранить на потом»", - "Remove user": "Удалить пользователя", - "Removed {{ count }} members_one": "Удалён {{ count }} участник", - "Removed {{ count }} members_few": "Удалены {{ count }} участника", - "Removed {{ count }} members_many": "Удалено {{ count }} участников", - "Removed {{ count }} members_other": "Удалено {{ count }} участников", - "Replied to a thread": "Ответил в ветке", - "Reply": "Ответить", - "Reply to {{ authorName }}": "Ответить {{ authorName }}", - "Reply to a message to start a thread": "Ответьте на сообщение, чтобы начать тред", - "Reply to Message": "Ответить на сообщение", - "replyCount_one": "1 ответ", - "replyCount_few": "{{ count }} ответов", - "replyCount_many": "{{ count }} ответов", - "replyCount_other": "{{ count }} ответов", - "Resend": "Отправить повторно", - "Retry upload": "Повторить загрузку", - "Review all options available in this poll": "Просмотрите все варианты, доступные в этом опросе", - "Review comments submitted with poll answers": "Просмотрите комментарии, отправленные вместе с ответами в опросе", - "Review poll results and open an option to see detailed votes": "Просмотрите результаты опроса и откройте вариант, чтобы увидеть подробные голоса", - "Review this message and choose whether to delete it, edit it, or send it anyway": "Просмотрите это сообщение и выберите, удалить его, отредактировать или отправить все равно", - "Review who voted for this option": "Просмотрите, кто проголосовал за этот вариант", - "Save": "Сохранить", - "Save for later": "Сохранить на потом", - "Saved for later": "Сохранено на потом", - "Search": "Поиск", - "Search GIFs": "Поиск GIF", - "search-results-header-filter-source-button-label--channels": "каналы", - "search-results-header-filter-source-button-label--messages": "сообщения", - "search-results-header-filter-source-button-label--users": "пользователи", - "Searching for {{ searchSourceType }}...": "Поиск {{ searchSourceType }}...", - "Searching...": "Ищем...", - "searchResultsCount_one": "1 результат", - "searchResultsCount_few": "{{ count }} результата", - "searchResultsCount_many": "{{ count }} результатов", - "searchResultsCount_other": "{{ count }} результатов", - "See all options ({{count}})_one": "Посмотреть все варианты ({{count}})", - "See all options ({{count}})_few": "Посмотреть все варианты ({{count}})", - "See all options ({{count}})_many": "Посмотреть все варианты ({{count}})", - "See all options ({{count}})_other": "Посмотреть все варианты ({{count}})", - "Select a thread to continue the conversation": "Выберите тред, чтобы продолжить беседу", - "Select more than one option": "Выберите более одного варианта", - "Select one": "Выберите один", - "Select one or more": "Выберите один или несколько", - "Select up to {{count}}_one": "Выберите до {{count}}", - "Select up to {{count}}_few": "Выберите до {{count}}", - "Select up to {{count}}_many": "Выберите до {{count}}", - "Select up to {{count}}_other": "Выберите до {{count}}", - "Select your current location and optionally enable live location sharing": "Выберите ваше текущее местоположение и при необходимости включите передачу геопозиции в реальном времени", - "Send": "Отправить", - "Send a message": "Отправьте сообщение", - "Send a message to start the conversation": "Отправьте сообщение, чтобы начать разговор", - "Send Anyway": "Мне всё равно, отправить", - "Send direct message": "Отправить личное сообщение", - "Send message request failed": "Не удалось отправить запрос на отправку сообщения", - "Send poll": "Отправить опрос", - "Sending...": "Отправка...", - "Sent": "Отправлено", - "Share": "Поделиться", - "Share a file to see it here": "Поделитесь файлом, чтобы увидеть его здесь", - "Share a photo or video to see it here": "Поделитесь фото или видео, чтобы увидеть их здесь", - "Share live location for": "Поделиться местоположением в прямом эфире на", - "Share Location": "Поделиться местоположением", - "Shared live location": "Общее местоположение в прямом эфире", - "Shared location": "Общее местоположение", - "Show all": "Показать все", - "Shuffle": "Перемешать", - "size limit": "ограничение размера", - "Slow Mode ON": "Медленный режим включен", - "Slow mode, wait {{ seconds }}s...": "Медленный режим: подождите {{ seconds }} с...", - "Some of the files will not be accepted": "Некоторые файлы не будут приняты", - "Start typing to search": "Начните вводить для поиска", - "Stop sharing": "Прекратить делиться", - "Submit": "Отправить", - "Suggest a new option to add to this poll": "Предложите новый вариант для добавления в этот опрос", - "Suggest an option": "Предложить вариант", - "Tap to remove": "Нажмите, чтобы удалить", - "Tap to remove: {{ reactionName }}": "Нажмите, чтобы удалить: {{ reactionName }}", - "Thinking...": "Думаю...", - "this content could not be displayed": "Этот контент не может быть отображен в данный момент", - "This field cannot be empty or contain only spaces": "Это поле не может быть пустым или содержать только пробелы", - "This message did not meet our content guidelines": "Сообщение не соответствует правилам", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "Этот пользователь снова сможет отправлять вам сообщения.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "Ветка", - "Thread has not been found": "Ветка не найдена", - "Thread reply": "Ответ в ветке", - "Thread Reply": "Ответ в ветке", - "ThreadListUnseenThreadsBanner/loading": "Загрузка...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} непрочитанная ветка", - "ThreadListUnseenThreadsBanner/unreadThreads_few": "{{ count }} непрочитанные ветки", - "ThreadListUnseenThreadsBanner/unreadThreads_many": "{{ count }} непрочитанных веток", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} непрочитанных веток", - "Threads": "Треды", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Вчера]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Сегодня]\", \"nextDay\": \"[Завтра]\", \"lastDay\": \"[Вчера]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[В прошлый] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "{{ count }} дн. назад", - "timestamp/relativeToday": "Сегодня", - "timestamp/relativeWeeksAgo": "{{ count }} нед. назад", - "timestamp/relativeYesterday": "Вчера", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Сегодня] [в] HH:mm\", \"nextDay\": \"[Завтра] [в] HH:mm\", \"lastDay\": \"[Вчера] [в] HH:mm\", \"nextWeek\": \"dddd [в] HH:mm\", \"lastWeek\": \"dddd [в] HH:mm\", \"sameElse\": \"ddd, D MMM [в] HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "Для начала записи разрешите доступ к камере в вашем браузере", - "To start recording, allow the microphone access in your browser": "Для начала записи разрешите доступ к микрофону в вашем браузере", - "totalVoteCount_one": "1 голос всего", - "totalVoteCount_few": "{{ count }} голоса всего", - "totalVoteCount_many": "{{ count }} голосов всего", - "totalVoteCount_other": "{{ count }} голосов всего", - "Translated": "Переведено", - "Translated from {{ language }}": "Переведено с {{ language }}", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "Введите число от 2 до 10", - "Type your message": "Ваше сообщение", - "Unarchive": "Удалить из архива", - "unban-command-args": "[@имяпользователя]", - "unban-command-description": "Разблокировать пользователя", - "Unblock": "Разблокировать", - "Unblock user": "Разблокировать пользователя", - "Unblock User": "Разблокировать пользователя", - "unknown error": "неизвестная ошибка", - "Unmute": "Включить уведомления", - "Unmute chat": "Включить уведомления чата", - "Unmute user": "Включить уведомления пользователя", - "unmute-command-args": "[@имяпользователя]", - "unmute-command-description": "Включить микрофон у пользователя", - "Unpin": "Открепить", - "Unread messages": "Непрочитанные сообщения", - "Unsupported attachment": "Неподдерживаемое вложение", - "unsupported file type": "неподдерживаемый тип файла", - "Update": "Обновить", - "Update the comment attached to your poll answer": "Обновите комментарий, прикрепленный к вашему ответу в опросе", - "Update your comment": "Обновите ваш комментарий", - "Upload blocked": "Загрузка заблокирована", - "Upload error": "Ошибка загрузки", - "Upload failed": "Загрузка не удалась", - "Upload Picture": "Загрузить изображение", - "Upload type: \"{{ type }}\" is not allowed": "Тип загрузки: \"{{ type }}\" не разрешен", - "User blocked": "Пользователь заблокирован", - "User muted": "Уведомления пользователя отключены", - "User removed": "Пользователь удалён", - "User unblocked": "Пользователь разблокирован", - "User unmuted": "Уведомления пользователя включены", - "User uploaded content": "Пользователь загрузил контент", - "Video": "Видео", - "videoCount_one": "{{ count }} видео", - "videoCount_few": "{{ count }} видео", - "videoCount_many": "{{ count }} видео", - "videoCount_other": "{{ count }} видео", - "View": "Просмотр", - "View {{count}} comments_one": "Просмотреть {{count}} комментарий", - "View {{count}} comments_few": "Просмотреть {{count}} комментариев", - "View {{count}} comments_many": "Просмотреть {{count}} комментариев", - "View {{count}} comments_other": "Просмотреть {{count}} комментариев", - "View all": "Показать все", - "View member details for {{ member }}": "Просмотреть сведения об участнике {{ member }}", - "View original": "Показать оригинал", - "View results": "Посмотреть результаты", - "View translation": "Показать перевод", - "Voice message": "Голосовое сообщение", - "Voice message {{ duration }}": "Голосовое сообщение {{ duration }}", - "Voice message deleted": "Голосовое сообщение удалено", - "voiceMessageCount_one": "{{ count }} голосовое сообщение", - "voiceMessageCount_few": "{{ count }} голосовых сообщения", - "voiceMessageCount_many": "{{ count }} голосовых сообщений", - "voiceMessageCount_other": "{{ count }} голосовых сообщений", - "Vote ended": "Голосование завершено", - "Votes": "Голоса", - "Wait until all attachments have uploaded": "Подождите, пока все вложения загрузятся", - "Waiting for network…": "Ожидание сети…", - "You": "Вы", - "You have no channels currently": "У вас нет каналов в данный момент", - "You've reached the maximum number of files": "Вы достигли максимального количества файлов" -} diff --git a/src/i18n/tr.json b/src/i18n/tr.json deleted file mode 100644 index ab985eb4be..0000000000 --- a/src/i18n/tr.json +++ /dev/null @@ -1,708 +0,0 @@ -{ - "{{ commaSeparatedUsers }} and {{ moreCount }} more": "{{ commaSeparatedUsers }} ve {{ moreCount }} daha", - "{{ commaSeparatedUsers }}, and {{ lastUser }}": "{{ commaSeparatedUsers }} ve {{ lastUser }}", - "{{ count }} files_one": "{{ count }} dosya", - "{{ count }} files_other": "{{ count }} dosya", - "{{ count }} members_one": "{{ count }} üye", - "{{ count }} members_other": "{{ count }} üye", - "{{ count }} members added_one": "{{ count }} üye eklendi", - "{{ count }} members added_other": "{{ count }} üye eklendi", - "{{ count }} people are typing_one": "{{ count }} kişi yazıyor", - "{{ count }} people are typing_many": "{{ count }} kişi yazıyor", - "{{ count }} people are typing_other": "{{ count }} kişi yazıyor", - "{{ count }} photos_one": "{{ count }} fotoğraf", - "{{ count }} photos_other": "{{ count }} fotoğraf", - "{{ count }} reactions_one": "{{ count }} tepki", - "{{ count }} reactions_other": "{{ count }} tepki", - "{{ count }} videos_one": "{{ count }} video", - "{{ count }} videos_other": "{{ count }} video", - "{{ firstUser }} and {{ secondUser }}": "{{ firstUser }} ve {{ secondUser }}", - "{{ imageCount }} more": "{{ imageCount }} adet daha", - "{{ member }} will be able to message you again.": "{{ member }} size tekrar mesaj gönderebilecek.", - "{{ member }} won't be able to message you anymore.": "{{ member }} artık size mesaj gönderemeyecek.", - "{{ memberCount }} members": "{{ memberCount }} üye", - "{{ typing }} are typing": "{{ typing }} yazıyor", - "{{ typing }} is typing": "{{ typing }} yazıyor", - "{{ user }} has been muted": "{{ user }} sessize alındı", - "{{ user }} has been unmuted": "{{ user }} sesi açıldı", - "{{ user }} is typing...": "{{ user }} yazıyor...", - "{{ users }} and {{ user }} are typing...": "{{ users }} ve {{ user }} yazıyor...", - "{{ users }} and more are typing...": "{{ users }} ve diğerleri yazıyor...", - "{{ watcherCount }} online": "{{ watcherCount }} çevrimiçi", - "{{count}} new messages_one": "{{count}} yeni mesaj", - "{{count}} new messages_other": "{{count}} yeni mesaj", - "{{count}} unread_one": "{{count}} okunmamış", - "{{count}} unread_other": "{{count}} okunmamış", - "{{count}} votes_one": "{{count}} oy", - "{{count}} votes_other": "{{count}} oy", - "+{{ imageCount }}": "+{{ imageCount }}", - "+{{count}} more options_one": "+{{count}} seçenek daha", - "+{{count}} more options_other": "+{{count}} seçenek daha", - "🏙 Attachment...": "🏙 Ek...", - "📊 {{createdBy}} created: {{ pollName}}": "📊 {{createdBy}} oluşturdu: {{ pollName}}", - "📊 {{votedBy}} voted: {{pollOptionText}}": "📊 {{votedBy}} oy verdi: {{pollOptionText}}", - "📍Shared location": "📍Paylaşılan konum", - "Actions": "Actions", - "Add": "Ekle", - "Add {{ count }} members_one": "{{ count }} üye ekle", - "Add {{ count }} members_other": "{{ count }} üye ekle", - "Add a comment": "Yorum ekle", - "Add a comment to your poll answer": "Anket yanıtınıza bir yorum ekleyin", - "Add an option": "Bir seçenek ekle", - "Add channel members": "Kanal üyeleri ekle", - "Add members": "Üye ekle", - "Add reaction": "Tepki ekle", - "Admin": "Yönetici", - "All results loaded": "Tüm sonuçlar yüklendi", - "Allow access to camera": "Kameraya erişime izin ver", - "Allow access to microphone": "Mikrofona erişime izin ver", - "Allow comments": "Yorumlara izin ver", - "Allow option suggestion": "Seçenek önerisine izin ver", - "Allow others to add comments": "Diğerlerinin yorum eklemesine izin ver", - "Already a member": "Zaten üye", - "Also send as a direct message": "Ayrıca doğrudan mesaj olarak gönder", - "Also send in channel": "Ayrıca kanala gönder", - "Also sent in channel": "Kanala da gönderildi", - "An error has occurred during recording": "Kayıt sırasında bir hata oluştu", - "An error has occurred during the recording processing": "Kayıt işlemi sırasında bir hata oluştu", - "Anonymous": "Anonim", - "Anonymous poll": "Anonim anket", - "Archive": "Arşivle", - "Are you sure you want to delete this message?": "Bu mesajı silmek istediğinizden emin misiniz?", - "Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?", - "aria/{{ count }} {{ suggestionsLabel }}_one": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} {{ suggestionsLabel }}_other": "{{ count }} {{ suggestionsLabel }}", - "aria/{{ count }} attachment_one": "{{ count }} ek", - "aria/{{ count }} attachment_other": "{{ count }} ek", - "aria/{{ count }} search results_one": "{{ count }} arama sonucu", - "aria/{{ count }} search results_other": "{{ count }} arama sonucu", - "aria/{{ count }} suggestions_one": "{{ count }} öneri", - "aria/{{ count }} suggestions_other": "{{ count }} öneri", - "aria/{{ count }} unread message_one": "{{ count }} okunmamış mesaj", - "aria/{{ count }} unread message_other": "{{ count }} okunmamış mesaj", - "aria/{{ setting }} disabled": "{{ setting }} devre dışı bırakıldı", - "aria/{{ setting }} enabled": "{{ setting }} etkinleştirildi", - "aria/Active": "Etkin", - "aria/Animated GIF": "Hareketli GIF", - "aria/Animated GIF: {{ title }}": "Hareketli GIF: {{ title }}", - "aria/Attachment": "Ek", - "aria/Attachment {{ attachmentType }}": "Ek {{ attachmentType }}", - "aria/Attachment Actions": "Ek işlemleri", - "aria/audio": "ses", - "aria/Audio position {{ elapsed }} of {{ duration }}": "Ses konumu {{ elapsed }} / {{ duration }}", - "aria/Audio position {{ progress }} percent": "Ses konumu yüzde {{ progress }}", - "aria/Back to attachments": "Eklere geri dön", - "aria/Back to parent menu button": "Üst menüye geri dön düğme", - "aria/Block User": "Kullanıcıyı engelle", - "aria/Bookmark Message": "Mesajı yer imi ekle", - "aria/Cancel recording": "Kaydı iptal et", - "aria/Cancel Reply": "Cevabı İptal Et", - "aria/Channel Actions": "Kanal işlemleri", - "aria/Channel details": "Kanal ayrıntıları", - "aria/Channel list": "Kanal listesi", - "aria/Chat view controls": "Sohbet görünümü kontrolleri", - "aria/Chat: {{ channelName }}": "Sohbet: {{ channelName }}", - "aria/Clear search": "Aramayı temizle", - "aria/Close callout dialog": "Bilgi balonu iletişim kutusunu kapat", - "aria/Close thread": "Konuyu kapat", - "aria/Collapse sidebar": "Kenar çubuğunu daralt", - "aria/Command activated: {{ command }}": "Komut etkinleştirildi: {{ command }}", - "aria/Command Suggestions": "Komut önerileri", - "aria/Complete recording": "Kaydı tamamla", - "aria/Copy Message Text": "Mesaj metnini kopyala", - "aria/Decrease value": "Değeri azalt", - "aria/Delete Message": "Mesajı sil", - "aria/Delivered": "İletildi", - "aria/Delivery status: {{ deliveryStatus }}": "Teslim durumu: {{ deliveryStatus }}", - "aria/Dismiss notification": "Bildirimi kapat", - "aria/Download attachment": "Ek indir", - "aria/Dropped \"{{ option }}\" at position {{ position }}.": "\"{{ option }}\" {{ position }}. konuma bırakıldı.", - "aria/Edit Message": "Mesajı Düzenle", - "aria/Emoji picker": "Emoji seçici", - "aria/Emoji Suggestions": "Emoji önerileri", - "aria/Exit search": "Aramadan çık", - "aria/Expand sidebar": "Kenar çubuğunu genişlet", - "aria/file": "dosya", - "aria/File upload": "Dosya yükleme", - "aria/Flag Message": "Mesajı bayrakla", - "aria/GIF": "GIF", - "aria/Giphy actions": "Giphy işlemleri", - "aria/Giphy canceled": "Giphy iptal edildi", - "aria/Giphy image changed": "Giphy görseli değişti", - "aria/Giphy image changed: {{ title }}": "Giphy görseli değişti: {{ title }}", - "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": "Giphy önizlemesi, yalnızca size görünür. Gönder, Karıştır veya İptal işlemlerini kullanın.", - "aria/Giphy sent": "Giphy gönderildi", - "aria/Go back": "Geri git", - "aria/image": "görsel", - "aria/Image failed to load": "Görsel yüklenemedi", - "aria/Increase value": "Değeri artır", - "aria/Jump to latest message": "En son mesaja git", - "aria/Jump to quoted message": "Alıntılanan mesaja git", - "aria/Last activity: {{ time }}": "Son etkinlik: {{ time }}", - "aria/Last message from {{ sender }}: {{ messagePreview }}": "{{ sender }} kişisinden son mesaj: {{ messagePreview }}", - "aria/Last message: {{ messagePreview }}": "Son mesaj: {{ messagePreview }}", - "aria/Mark Message Unread": "Okunmamış olarak işaretle", - "aria/Mark messages as read": "Mesajları okundu olarak işaretle", - "aria/Mention Suggestions": "Bahsetme önerileri", - "aria/Message Actions": "Mesaj eylemleri", - "aria/Message from {{ user }},": "{{ user }} adlı kullanıcıdan mesaj,", - "aria/Message input": "Mesaj girişi", - "aria/Message with attachments": "Ekli mesaj", - "aria/Message,": "Mesaj,", - "aria/Mute User": "Kullanıcıyı sustur", - "aria/Next page": "Sonraki sayfa", - "aria/No search results found": "Arama sonucu bulunamadı", - "aria/Notifications": "Bildirimler", - "aria/Open Attachment Selector": "Ek Seçiciyi Aç", - "aria/Open Channel Actions Menu": "Kanal işlemleri menüsünü aç", - "aria/Open channel details": "Kanal ayrıntılarını aç", - "aria/Open channels view": "Kanal görünümünü aç", - "aria/Open image shared by {{ name }}": "{{ name }} tarafından paylaşılan resmi aç", - "aria/Open Message Actions Menu": "Mesaj İşlemleri Menüsünü Aç", - "aria/Open Reaction Selector": "Tepki Seçiciyi Aç", - "aria/Open Thread": "Konuyu Aç", - "aria/Open threads view": "Konu görünümünü aç", - "aria/Open threads view with unread threads_one": "Konu görünümünü aç, {{ count }} okunmamış konu", - "aria/Open threads view with unread threads_other": "Konu görünümünü aç, {{ count }} okunmamış konu", - "aria/Open video shared by {{ name }}": "{{ name }} tarafından paylaşılan videoyu aç", - "aria/Opened channel: {{ name }}": "Kanal açıldı: {{ name }}", - "aria/Opened thread in {{ name }}": "{{ name }} içinde konu açıldı", - "aria/Option {{ position }}": "Seçenek {{ position }}", - "aria/Options can now be reordered and removed.": "Seçenekler artık yeniden sıralanabilir ve kaldırılabilir.", - "aria/Pause": "Duraklat", - "aria/Pause recording": "Kaydı duraklat", - "aria/Percent complete": "Yüzde {{percent}} tamamlandı", - "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": "\"{{ option }}\" alındı. Yeniden sıralamak için ok tuşlarını kullanın. Bırakmak için Boşluk veya Sekme tuşuna basın.", - "aria/Pin Message": "Mesajı sabitle", - "aria/Play": "Oynat", - "aria/Poll dialog opened": "Anket iletişim kutusu açıldı", - "aria/Poll sent": "Anket gönderildi", - "aria/Poll: {{ pollName }}": "Anket: {{ pollName }}", - "aria/Press Enter to start typing": "Yazmaya başlamak için Enter'a basın", - "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": "Bu seçeneği seçmek için Boşluk tuşuna basın, taşımak için Yukarı ve Aşağı ok tuşlarını kullanın, ardından seçimi kaldırmak için Boşluk tuşuna tekrar basın.", - "aria/Previous page": "Önceki sayfa", - "aria/Quote Message": "Mesajı alıntıla", - "aria/Reaction list": "Tepki listesi", - "aria/Read": "Okundu", - "aria/Recording paused": "Kayıt duraklatıldı", - "aria/Recording resumed": "Kayıt devam ettirildi", - "aria/Recording started": "Kayıt başladı", - "aria/Remind Me Message": "Hatırlat", - "aria/Remove attachment": "Eki kaldır", - "aria/Remove location attachment": "Konum ekini kaldır", - "aria/Remove option: {{ option }}": "Seçeneği kaldır: {{ option }}", - "aria/Remove Reminder": "Hatırlatıcıyı kaldır", - "aria/Remove Save For Later": "Sonraya kaydet'i kaldır", - "aria/Removed option {{ option }}": "{{ option }} seçeneği kaldırıldı", - "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": "\"{{ option }}\" sırasını değiştir, {{ total }} konumdan {{ position }}.", - "aria/Reorder option {{ position }}": "Seçenek {{ position }} sırasını değiştir", - "aria/Resend Message": "Mesajı tekrar gönder", - "aria/Resume recording": "Kaydı devam ettir", - "aria/Retry upload": "Yüklemeyi Tekrar Dene", - "aria/Review bounced message": "Geri dönen mesajı incele", - "aria/Search cleared": "Arama temizlendi", - "aria/Search results": "Arama sonuçları", - "aria/Search results header filter button": "Arama sonuçları başlık filtre düğmesi", - "aria/Search results header filter button for: {{ source }}": "{{ source }} için arama sonuçları başlık filtre düğmesi", - "aria/Seek audio position": "Ses konumunu ara", - "aria/Select Reaction: {{ reactionName }}": "Tepki seç: {{ reactionName }}", - "aria/Select User Channel: {{ name }}": "Kullanıcı kanalını seç: {{ name }}", - "aria/Send": "Gönder", - "aria/Sent": "Gönderildi", - "aria/Shared a link": "Bağlantı paylaşıldı", - "aria/Shared a link with title: {{ linkTitle }}": "Başlıklı bağlantı paylaşıldı: {{ linkTitle }}", - "aria/Shared location": "Paylaşılan konum", - "aria/Show preview": "Önizlemeyi göster", - "aria/Start recording audio": "Ses kaydını başlat", - "aria/Stop AI Generation": "Yapay Zeka Üretimini Durdur", - "aria/Submenu": "Alt menü", - "aria/Suggestions": "Öneriler", - "aria/There are no messages in this chat.": "Bu sohbette mesaj yok", - "aria/This option can be reordered and removed.": "Bu seçenek yeniden sıralanabilir ve kaldırılabilir.", - "aria/Thread list": "İleti dizisi listesi", - "aria/Thread: {{ messagePreview }}": "Konu: {{ messagePreview }}", - "aria/Unblock User": "Kullanıcının engelini kaldır", - "aria/Unmute User": "Sesini aç", - "aria/Unpin Message": "Sabitlemeyi kaldır", - "aria/User selected: {{ user }}": "Seçilen kullanıcı: {{ user }}", - "aria/video": "video", - "aria/voice message": "sesli mesaj", - "aria/Voice message sent": "Sesli mesaj gönderildi", - "aria/Voice recording attached": "Ses kaydı eklendi", - "Ask a question": "Bir soru sor", - "Attach": "Ekle", - "Attach files": "Dosya ekle", - "Attachment": "Ek", - "Attachment upload blocked due to {{reason}}": "{{reason}} nedeniyle ek yükleme engellendi", - "Attachment upload failed due to {{reason}}": "{{reason}} nedeniyle ek yükleme başarısız oldu", - "Back": "Geri", - "ban-command-args": "[@kullanıcıadı] [metin]", - "ban-command-description": "Bir kullanıcıyı yasakla", - "Block user": "Kullanıcıyı engelle", - "Block User": "Kullanıcıyı engelle", - "Browse channel members": "Kanal üyelerine göz at", - "Browse pinned messages": "Sabitlenmiş mesajlara göz at", - "Cancel": "İptal", - "Cannot seek in the recording": "Kayıtta arama yapılamıyor", - "Changes saved": "Değişiklikler kaydedildi", - "Channel archived": "Kanal arşivlendi", - "Channel members": "Kanal üyeleri", - "Channel Missing": "Kanal bulunamıyor", - "Channel muted": "Kanal sessize alındı", - "Channel pinned": "Kanal sabitlendi", - "Channel unarchived": "Kanal arşivden çıkarıldı", - "Channel unmuted": "Kanal sesi açıldı", - "Channel unpinned": "Kanal sabitlemesi kaldırıldı", - "Channels": "Kanallar", - "Chat deleted": "Chat deleted", - "Chats": "Sohbetler", - "Choose between 2 to 10 options": "2 ile 10 seçenek arasından seçin", - "Close": "Kapat", - "Close dialog": "İletişim kutusunu kapat", - "Close emoji picker": "Emoji seçiciyi kapat", - "Command not available while editing": "Düzenleme sırasında komut kullanılamaz", - "Command not available while replying": "Yanıtlama sırasında komut kullanılamaz", - "Commands": "Komutlar", - "Commands matching": "Eşleşen komutlar", - "Connection failure, reconnecting now...": "Bağlantı hatası, tekrar bağlanılıyor...", - "Contact info": "İletişim bilgileri", - "Contact name": "Kişi adı", - "Copy Message": "Mesajı kopyala", - "Create": "Oluştur", - "Create a question, add options, and configure poll settings": "Bir soru oluşturun, seçenekler ekleyin ve anket ayarlarını yapılandırın", - "Create poll": "Anket oluştur", - "Current location": "Mevcut konum", - "Delete": "Sil", - "Delete chat": "Sohbeti sil", - "Delete for me": "Benim için sil", - "Delete message": "Mesajı sil", - "Delivered": "İletildi", - "Direct message": "Doğrudan mesaj", - "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": "Bu anketi şimdi sonlandırmak istiyor musunuz? Sonlandırdıktan sonra bu ankette kimse oy kullanamayacak.", - "Download {{ fileName }}": "{{ fileName }} dosyasını indir", - "Download All": "Tümünü indir", - "Download Attachment": "Eki indir", - "Download attachment {{ name }}": "Ek {{ name }}'i indir", - "Download attachment {{ number }}": "{{ number }} numaralı eki indir", - "Drag your files here": "Dosyalarınızı buraya sürükleyin", - "Drag your files here to add to your post": "Gönderinize eklemek için dosyalarınızı buraya sürükleyin", - "Due {{ timeLeft }}": "{{ timeLeft }} içinde süresi dolacak", - "Due since {{ dueSince }}": "{{ dueSince }}'den beri süresi dolmuş", - "duration/Message reminder": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Remind Me": "{{ milliseconds | durationFormatter(withSuffix: true) }}", - "duration/Share Location": "{{ milliseconds | durationFormatter }}", - "Edit": "Düzenle", - "Edit chat data": "Sohbet verilerini düzenle", - "Edit contact": "Kişiyi düzenle", - "Edit group": "Grubu düzenle", - "Edit Message": "Mesajı Düzenle", - "Edit message request failed": "Mesaj düzenleme isteği başarısız oldu", - "Edited": "Düzenlendi", - "Emoji matching": "Emoji eşleştirme", - "Empty message...": "Boş mesaj...", - "End": "Son", - "End poll": "Anketi sonlandır", - "End this poll?": "Bu anketi sonlandır?", - "End vote": "Oyu bitir", - "Enforce unique vote is enabled": "Benzersiz oy etkinleştirildi", - "Error": "Hata", - "Error · Unsent": "Hata · Gönderilemedi", - "Error adding flag": "Bayrak eklenirken hata oluştu", - "Error adding members": "Error adding members", - "Error blocking user": "Kullanıcı engellenirken hata oluştu", - "Error connecting to chat, refresh the page to try again.": "Bağlantı hatası, sayfayı yenileyip tekrar deneyin.", - "Error deleting chat": "Error deleting chat", - "Error deleting message": "Mesaj silinirken hata oluştu", - "Error fetching reactions": "Reaksiyonlar alınırken hata oluştu", - "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": "Mesajı okunmamış olarak işaretleme hatası. En yeni 100 kanal mesajından daha eski okunmamış mesajları işaretleme yapılamaz.", - "Error muting a user ...": "Kullanıcıyı sessize alırken hata oluştu ...", - "Error muting channel": "Kanal sessize alınırken hata oluştu", - "Error muting user": "Kullanıcı sessize alınırken hata oluştu", - "Error opening direct message": "Direkt mesaj açılırken hata oluştu", - "Error pinning message": "Mesaj sabitlenirken hata oluştu", - "Error removing members": "Üyeler kaldırılırken hata oluştu", - "Error removing message pin": "Mesaj PIN'i kaldırılırken hata oluştu", - "Error removing user": "Kullanıcı kaldırılırken hata oluştu", - "Error reproducing the recording": "Kaydı yeniden üretme hatası", - "Error starting recording": "Kayıt başlatılırken hata oluştu", - "Error unblocking user": "Kullanıcının engeli kaldırılırken hata oluştu", - "Error unmuting a user ...": "Kullanıcının sesini açarken hata oluştu ...", - "Error unmuting channel": "Kanalın sesi açılırken hata oluştu", - "Error unmuting user": "Kullanıcının sesi açılırken hata oluştu", - "Error uploading attachment": "Ek yüklenirken hata oluştu", - "Error uploading file": "Dosya yüklenirken hata oluştu", - "Error uploading image": "Resmi yüklerken hata", - "Error: {{ errorMessage }}": "Hata: {{ errorMessage }}", - "Exit command {{ command }}": "Komuttan çık {{ command }}", - "Failed to block user": "Kullanıcı engellenemedi", - "Failed to create the poll": "Anket oluşturulurken hata oluştu", - "Failed to create the poll due to {{reason}}": "{{reason}} nedeniyle anket oluşturulamadı", - "Failed to delete the message": "Mesaj silinemedi", - "Failed to end the poll": "Anket sonlandırılamadı", - "Failed to end the poll due to {{reason}}": "{{reason}} nedeniyle anket sonlandırılamadı", - "Failed to jump to the first unread message": "İlk okunmamış mesaja atlamada hata oluştu", - "Failed to leave channel": "Kanaldan çıkılamadı", - "Failed to load channels": "Kanallar yüklenemedi", - "Failed to load more channels": "Daha fazla kanal yüklenemedi", - "Failed to mark channel as read": "Kanalı okundu olarak işaretleme başarısız oldu", - "Failed to play the recording": "Kayıt oynatılamadı", - "Failed to retrieve location": "Konum alınamadı", - "Failed to save changes": "Değişiklikler kaydedilemedi", - "Failed to share location": "Konum paylaşılamadı", - "Failed to update channel archive status": "Kanalın arşiv durumu güncellenemedi", - "Failed to update channel mute status": "Kanalın sessiz durumu güncellenemedi", - "Failed to update channel pinned status": "Kanalın sabitleme durumu güncellenemedi", - "File": "Dosya", - "File is required for upload attachment": "Ek yüklemek için dosya gerekli", - "File is too large: {{ size }}, maximum upload size is {{ limit }}": "Dosya çok büyük: {{ size }}, maksimum yükleme boyutu {{ limit }}", - "File too large": "Dosya çok büyük", - "fileCount_one": "1 dosya", - "fileCount_other": "{{ count }} dosya", - "Files": "Dosyalar", - "Flag": "Bayrak", - "Generating...": "Oluşturuluyor...", - "giphy-command-args": "[metin]", - "giphy-command-description": "Rastgele bir gif'i kanala gönder", - "Go back": "Geri dön", - "Group info": "Grup bilgileri", - "Group name": "Grup adı", - "Hide who voted": "Kimin oy verdiğini gizle", - "Image": "Görsel", - "imageCount_one": "Görsel", - "imageCount_other": "{{ count }} görsel", - "Instant commands": "Anlık komutlar", - "language/af": "Afrikanca", - "language/am": "Amharca", - "language/ar": "Arapça", - "language/az": "Azerice", - "language/bg": "Bulgarca", - "language/bn": "Bengalce", - "language/bs": "Boşnakça", - "language/cs": "Çekçe", - "language/da": "Danca", - "language/de": "Almanca", - "language/el": "Yunanca", - "language/en": "İngilizce", - "language/es": "İspanyolca", - "language/es-MX": "İspanyolca (Meksika)", - "language/et": "Estonyaca", - "language/fa": "Farsça", - "language/fa-AF": "Darice", - "language/fi": "Fince", - "language/fr": "Fransızca", - "language/fr-CA": "Fransızca (Kanada)", - "language/ha": "Hausaca", - "language/he": "İbranice", - "language/hi": "Hintçe", - "language/hr": "Hırvatça", - "language/ht": "Haiti Kreolcesi", - "language/hu": "Macarca", - "language/id": "Endonezce", - "language/it": "İtalyanca", - "language/ja": "Japonca", - "language/ka": "Gürcüce", - "language/ko": "Korece", - "language/lt": "Litvanca", - "language/lv": "Letonca", - "language/ms": "Malayca", - "language/nl": "Felemenkçe", - "language/no": "Norveççe", - "language/pl": "Lehçe", - "language/ps": "Peştuca", - "language/pt": "Portekizce", - "language/ro": "Romence", - "language/ru": "Rusça", - "language/sk": "Slovakça", - "language/sl": "Slovence", - "language/so": "Somalice", - "language/sq": "Arnavutça", - "language/sr": "Sırpça", - "language/sv": "İsveççe", - "language/sw": "Svahilice", - "language/ta": "Tamilce", - "language/th": "Tayca", - "language/tl": "Tagalogca", - "language/tr": "Türkçe", - "language/uk": "Ukraynaca", - "language/ur": "Urduca", - "language/vi": "Vietnamca", - "language/zh": "Çince (basitleştirilmiş)", - "language/zh-TW": "Çince (geleneksel)", - "Last seen {{ timestamp }}": "Son görülme {{ timestamp }}", - "Leave Channel": "Kanaldan ayrıl", - "Leave chat": "Kanaldan ayrıl", - "Left channel": "Kanaldan ayrıldınız", - "Let others add options": "Başkalarının seçenek eklemesine izin ver", - "Limit votes per person": "Kişi başına oy sınırı", - "Link": "Bağlantı", - "linkCount_one": "Bağlantı", - "linkCount_other": "{{ count }} bağlantı", - "live": "canlı", - "Live for {{duration}}": "{{duration}} boyunca canlı", - "Live location": "Canlı konum", - "Live until {{ timestamp }}": "{{ timestamp }}'e kadar canlı", - "Load more": "Daha fazla yükle", - "Local upload attachment missing local id": "Yerel yükleme ekinde yerel kimlik eksik", - "Location": "Konum", - "Location sharing ended": "Konum paylaşımı sona erdi", - "Location: {{ coordinates }}": "Konum: {{ coordinates }}", - "Manage channel": "Kanalı yönet", - "Manage members": "Üyeleri yönet", - "Mark as unread": "Okunmamış olarak işaretle", - "Maximum number of votes (from 2 to 10)": "Maksimum oy sayısı (2 ile 10 arası)", - "Maximum votes per person": "Kişi başına maksimum oy", - "Member detail": "Üye detayı", - "mention/Channel": "Kanal", - "mention/Channel Description": "Bu kanaldaki herkesi bildir", - "mention/Here": "Burada", - "mention/Here Description": "Bu kanaldaki tüm çevrimiçi üyeleri bildir", - "Menu": "Menü", - "Message deleted": "Mesaj silindi", - "Message Failed · Click to try again": "Mesaj Başarısız · Tekrar denemek için tıklayın", - "Message Failed · Unauthorized": "Mesaj Başarısız · Yetkisiz", - "Message failed to send": "Mesaj gönderilemedi", - "Message has been successfully flagged": "Mesaj başarıyla bayraklandı", - "Message marked as unread": "Mesaj okunmadı olarak işaretlendi", - "Message pinned": "Mesaj sabitlendi", - "Message unpinned": "Mesaj sabitlemesi kaldırıldı", - "Message was blocked by moderation policies": "Mesaj moderasyon politikaları tarafından engellendi", - "Messages have been marked unread.": "Mesajlar okunmamış olarak işaretlendi.", - "Missing permissions to upload the attachment": "Ek yüklemek için izinler eksik", - "Moderator": "Moderatör", - "Multiple votes": "Çoklu oy", - "Mute": "Sessiz", - "Mute chat": "Sohbeti sessize al", - "Mute user": "Kullanıcıyı sessize al", - "mute-command-args": "[@kullanıcıadı]", - "mute-command-description": "Bir kullanıcının sesini kapat", - "network error": "ağ hatası", - "New": "Yeni", - "New message from {{user}}": "{{user}} adlı kullanıcıdan yeni mesaj", - "New Messages!": "Yeni Mesajlar!", - "Next": "İleri", - "Next image": "Sonraki görsel", - "No chats here yet…": "Henüz burada sohbet yok...", - "No conversations yet": "Henüz konuşma yok", - "No files": "Dosya yok", - "No items exist": "Hiç öğe yok", - "No member found": "Üye bulunamadı", - "No messages found": "Mesaj bulunamadı", - "No photos or videos": "Fotoğraf veya video yok", - "No pinned messages": "Sabitlenmiş mesaj yok", - "No results found": "Sonuç bulunamadı", - "No user found": "Kullanıcı bulunamadı", - "Nobody will be able to vote in this poll anymore.": "Artık bu ankette kimse oy kullanamayacak.", - "Nothing yet...": "Şimdilik hiçbir şey...", - "Notify all {{ role }} members": "{{ role }} rolündeki tüm üyelere bildir", - "Offline": "Çevrimdışı", - "Ok": "Tamam", - "Online": "Çevrimiçi", - "Only numbers are allowed": "Sadece sayılar kullanılabilir", - "Only visible to you": "Sadece sana görünür", - "Open emoji picker": "Emoji klavyesini aç", - "Open gallery at image {{ index }}": "Galeriyi {{ index }}. görselde aç", - "Open image in gallery": "Görseli galeride aç", - "Open location in a map": "Konumu haritada aç", - "Open members actions": "Open members actions", - "Open menu": "Menüyü aç", - "Option already exists": "Seçenek zaten mevcut", - "Option is empty": "Seçenek boş", - "Options": "Seçenekler", - "Original": "Orijinal", - "Owner": "Sahip", - "People matching": "Eşleşen kişiler", - "Photo": "Fotoğraf", - "Photos & videos": "Fotoğraflar ve videolar", - "Pin": "Sabitle", - "Pin a message to see it here": "Burada görmek için bir mesaj sabitle", - "Pinned by {{ name }}": "{{ name }} sabitledi", - "Pinned by You": "Sizin sabitlediğiniz", - "Pinned message": "Sabitlenmiş mesaj", - "Pinned messages": "Sabitlenmiş mesajlar", - "placeholder/PollComment": "Yorumunuz", - "placeholder/PollOptionSuggestion": "Yeni bir seçenek girin", - "Play video": "Videoyu oynat", - "Playback speed {{ rate }}x": "Oynatma hızı {{ rate }}x", - "Poll": "Anket", - "Poll comments": "Anket yorumları", - "Poll ended": "Anket sonlandı", - "Poll options": "Anket seçenekleri", - "Poll results": "Anket sonuçları", - "Poll sent": "Anket gönderildi", - "Previous": "Geri", - "Previous image": "Önceki görsel", - "Question": "Soru", - "Question {{ optionOrderNumber}}": "Soru {{ optionOrderNumber}}", - "Question is required": "Soru gereklidir", - "Quote Reply": "Alıntıyla yanıtla", - "Reached the vote limit. Remove an existing vote first.": "Oylama sınırına ulaşıldı. Önce mevcut bir oyu kaldırın.", - "Recording format is not supported and cannot be reproduced": "Kayıt formatı desteklenmiyor ve çoğaltılamıyor", - "Remind me": "Bana hatırlat", - "Remind Me": "Hatırlat", - "Reminder set": "Hatırlatıcı ayarlandı", - "Remove": "Kaldır", - "Remove {{ count }} members_one": "{{ count }} üyeyi kaldır", - "Remove {{ count }} members_other": "{{ count }} üyeyi kaldır", - "Remove {{ member }} from this channel?": "{{ member }} bu kanaldan kaldırılsın mı?", - "Remove channel members": "Kanal üyelerini kaldır", - "Remove reminder": "Hatırlatıcıyı kaldır", - "Remove save for later": "Sonraya kaydet'i kaldır", - "Remove user": "Kullanıcıyı kaldır", - "Removed {{ count }} members_one": "{{ count }} üye kaldırıldı", - "Removed {{ count }} members_other": "{{ count }} üye kaldırıldı", - "Replied to a thread": "Bir iş parçacığına yanıt verdi", - "Reply": "Cevapla", - "Reply to {{ authorName }}": "{{ authorName }} kişisine yanıt ver", - "Reply to a message to start a thread": "Bir thread başlatmak için bir mesaja yanıt verin", - "Reply to Message": "Mesaja Cevapla", - "replyCount_one": "1 cevap", - "replyCount_other": "{{ count }} cevap", - "Resend": "Tekrar gönder", - "Retry upload": "Yüklemeyi yeniden dene", - "Review all options available in this poll": "Bu anketteki tüm mevcut seçenekleri inceleyin", - "Review comments submitted with poll answers": "Anket yanıtlarıyla gönderilen yorumları inceleyin", - "Review poll results and open an option to see detailed votes": "Anket sonuçlarını inceleyin ve ayrıntılı oyları görmek için bir seçenek açın", - "Review this message and choose whether to delete it, edit it, or send it anyway": "Bu mesajı inceleyin ve silmeyi, düzenlemeyi veya yine de göndermeyi seçin", - "Review who voted for this option": "Bu seçenek için kimlerin oy verdiğini inceleyin", - "Save": "Kaydet", - "Save for later": "Daha sonra kaydet", - "Saved for later": "Daha sonra kaydedildi", - "Search": "Arama", - "Search GIFs": "GIF ara", - "search-results-header-filter-source-button-label--channels": "kanallar", - "search-results-header-filter-source-button-label--messages": "mesajlar", - "search-results-header-filter-source-button-label--users": "kullanıcılar", - "Searching for {{ searchSourceType }}...": "{{ searchSourceType }} aranıyor...", - "Searching...": "Aranıyor...", - "searchResultsCount_one": "1 sonuç", - "searchResultsCount_other": "{{ count }} sonuç", - "See all options ({{count}})_one": "Tüm seçenekleri göster ({{count}})", - "See all options ({{count}})_other": "Tüm seçenekleri göster ({{count}})", - "Select a thread to continue the conversation": "Sohbeti sürdürmek için bir ileti dizisi seçin", - "Select more than one option": "Birden fazla seçenek seçin", - "Select one": "Birini seçin", - "Select one or more": "Bir veya daha fazlasını seçin", - "Select up to {{count}}_one": "En fazla {{count}}'yi seçin", - "Select up to {{count}}_other": "En fazla {{count}}'yi seçin", - "Select your current location and optionally enable live location sharing": "Mevcut konumunuzu seçin ve isteğe bağlı olarak canlı konum paylaşımını etkinleştirin", - "Send": "Gönder", - "Send a message": "Bir mesaj gönderin", - "Send a message to start the conversation": "Sohbete başlamak için bir mesaj gönderin", - "Send Anyway": "Yine de gönder", - "Send direct message": "Direkt mesaj gönder", - "Send message request failed": "Mesaj gönderme isteği başarısız oldu", - "Send poll": "Anketi gönder", - "Sending...": "Gönderiliyor...", - "Sent": "Gönderildi", - "Share": "Paylaş", - "Share a file to see it here": "Burada görmek için bir dosya paylaşın", - "Share a photo or video to see it here": "Burada görmek için bir fotoğraf veya video paylaşın", - "Share live location for": "Canlı konum paylaş", - "Share Location": "Konum Paylaş", - "Shared live location": "Paylaşılan canlı konum", - "Shared location": "Paylaşılan konum", - "Show all": "Tümünü göster", - "Shuffle": "Karıştır", - "size limit": "boyut sınırı", - "Slow Mode ON": "Yavaş Mod Açık", - "Slow mode, wait {{ seconds }}s...": "Yavaş mod, {{ seconds }} sn bekleyin...", - "Some of the files will not be accepted": "Bazı dosyalar kabul edilmeyecek", - "Start typing to search": "Aramak için yazmaya başlayın", - "Stop sharing": "Paylaşımı durdur", - "Submit": "Gönder", - "Suggest a new option to add to this poll": "Bu ankete eklenecek yeni bir seçenek önerin", - "Suggest an option": "Bir seçenek önerin", - "Tap to remove": "Kaldırmak için dokunun", - "Tap to remove: {{ reactionName }}": "Kaldırmak için dokunun: {{ reactionName }}", - "Thinking...": "Düşünüyor...", - "this content could not be displayed": "bu içerik gösterilemiyor", - "This field cannot be empty or contain only spaces": "Bu alan boş olamaz veya sadece boşluk içeremez", - "This message did not meet our content guidelines": "Bu mesaj içerik yönergelerimize uygun değil", - "This permanently deletes your message history with {{ user }}. This can't be undone.": "This permanently deletes your message history with {{ user }}. This can't be undone.", - "This user will be able to message you again.": "Bu kullanıcı size tekrar mesaj gönderebilecek.", - "This user won't be able to message you anymore. You can unblock them anytime.": "This user won't be able to message you anymore. You can unblock them anytime.", - "Thread": "Konu", - "Thread has not been found": "Konu bulunamadı", - "Thread reply": "Konu yanıtı", - "Thread Reply": "Konu yanıtı", - "ThreadListUnseenThreadsBanner/loading": "Yükleniyor...", - "ThreadListUnseenThreadsBanner/unreadThreads_one": "{{ count }} okunmamış ileti dizisi", - "ThreadListUnseenThreadsBanner/unreadThreads_other": "{{ count }} okunmamış ileti dizisi", - "Threads": "İleti dizileri", - "timestamp/ChannelDetailPinnedMessageTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Yesterday]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/ChannelMembersLastActive": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/ChannelPreviewTimestamp": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"LT\", \"lastDay\": \"[Dün]\", \"lastWeek\": \"dddd\", \"sameElse\": \"L\" }) }}", - "timestamp/DateSeparator": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Bugün]\", \"nextDay\": \"[Yarın]\", \"lastDay\": \"[Dün]\", \"nextWeek\": \"dddd\", \"lastWeek\": \"[Geçen] dddd\", \"sameElse\": \"ddd, D MMM\" }) }}", - "timestamp/LiveLocation": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(calendar: false; format: HH:mm) }}", - "timestamp/PollVote": "{{ timestamp | timestampFormatter(relativeCompact: true) }}", - "timestamp/PollVoteTooltip": "{{ timestamp | timestampFormatter(calendar: true) }}", - "timestamp/relativeDaysAgo": "{{ count }} g önce", - "timestamp/relativeToday": "Bugün", - "timestamp/relativeWeeksAgo": "{{ count }} hf önce", - "timestamp/relativeYesterday": "Dün", - "timestamp/ReminderNotification": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { \"sameDay\": \"[Bugün] [saat] HH:mm\", \"nextDay\": \"[Yarın] [saat] HH:mm\", \"lastDay\": \"[Dün] [saat] HH:mm\", \"nextWeek\": \"dddd [saat] HH:mm\", \"lastWeek\": \"[Geçen] dddd [saat] HH:mm\", \"sameElse\": \"ddd, D MMM [saat] HH:mm\" }) }}", - "timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: dddd L) }}", - "To start recording, allow the camera access in your browser": "Kayıt yapmaya başlamak için tarayıcınızda kameraya erişime izin verin", - "To start recording, allow the microphone access in your browser": "Kayıt yapmaya başlamak için tarayıcınızda mikrofona erişime izin verin", - "totalVoteCount_one": "toplam 1 oy", - "totalVoteCount_other": "toplam {{ count }} oy", - "Translated": "Çevrildi", - "Translated from {{ language }}": "{{ language }} dilinden çevrildi", - "translationBuilderTopic/notification": "{{value, notification}}", - "Type a number from 2 to 10": "2 ile 10 arasında bir sayı yazın", - "Type your message": "Mesajınızı yazın", - "Unarchive": "Arşivden çıkar", - "unban-command-args": "[@kullanıcıadı]", - "unban-command-description": "Bir kullanıcının yasağını kaldır", - "Unblock": "Engeli kaldır", - "Unblock user": "Kullanıcının engelini kaldır", - "Unblock User": "Kullanıcının engelini kaldır", - "unknown error": "bilinmeyen hata", - "Unmute": "Sesini aç", - "Unmute chat": "Sohbetin sesini aç", - "Unmute user": "Kullanıcının sesini aç", - "unmute-command-args": "[@kullanıcıadı]", - "unmute-command-description": "Bir kullanıcının sesini aç", - "Unpin": "Sabitlemeyi kaldır", - "Unread messages": "Okunmamış mesajlar", - "Unsupported attachment": "Desteklenmeyen ek", - "unsupported file type": "desteklenmeyen dosya türü", - "Update": "Güncelle", - "Update the comment attached to your poll answer": "Anket yanıtınıza ekli yorumu güncelleyin", - "Update your comment": "Yorumunuzu güncelleyin", - "Upload blocked": "Yükleme engellendi", - "Upload error": "Yükleme hatası", - "Upload failed": "Yükleme başarısız oldu", - "Upload Picture": "Resim yükle", - "Upload type: \"{{ type }}\" is not allowed": "Yükleme türü: \"{{ type }}\" izin verilmez", - "User blocked": "Kullanıcı engellendi", - "User muted": "Kullanıcı sessize alındı", - "User removed": "Kullanıcı kaldırıldı", - "User unblocked": "Kullanıcının engeli kaldırıldı", - "User unmuted": "Kullanıcının sesi açıldı", - "User uploaded content": "Kullanıcı tarafından yüklenen içerik", - "Video": "Video", - "videoCount_one": "Video", - "videoCount_other": "{{ count }} video", - "View": "Görüntüle", - "View {{count}} comments_one": "{{count}} yorumu görüntüle", - "View {{count}} comments_other": "{{count}} yorumu görüntüle", - "View all": "Tümünü görüntüle", - "View member details for {{ member }}": "{{ member }} için üye detaylarını görüntüle", - "View original": "Orijinali görüntüle", - "View results": "Sonuçları görüntüle", - "View translation": "Çeviriyi görüntüle", - "Voice message": "Sesli mesaj", - "Voice message {{ duration }}": "Sesli mesaj {{ duration }}", - "Voice message deleted": "Sesli mesaj silindi", - "voiceMessageCount_one": "Sesli mesaj", - "voiceMessageCount_other": "{{ count }} sesli mesaj", - "Vote ended": "Oylama sona erdi", - "Votes": "Oylar", - "Wait until all attachments have uploaded": "Tüm ekler yüklenene kadar bekleyin", - "Waiting for network…": "Ağ bekleniyor…", - "You": "Sen", - "You have no channels currently": "Henüz kanalınız yok", - "You've reached the maximum number of files": "Maksimum dosya sayısına ulaştınız" -} diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts deleted file mode 100644 index c01bb9bed1..0000000000 --- a/src/i18n/translations.ts +++ /dev/null @@ -1,27 +0,0 @@ -import deTranslations from './de.json'; -import enTranslations from './en.json'; -import esTranslations from './es.json'; -import frTranslations from './fr.json'; -import hiTranslations from './hi.json'; -import itTranslations from './it.json'; -import jaTranslations from './ja.json'; -import koTranslations from './ko.json'; -import nlTranslations from './nl.json'; -import ptTranslations from './pt.json'; -import ruTranslations from './ru.json'; -import trTranslations from './tr.json'; - -export { - deTranslations, - enTranslations, - esTranslations, - frTranslations, - hiTranslations, - itTranslations, - jaTranslations, - koTranslations, - nlTranslations, - ptTranslations, - ruTranslations, - trTranslations, -}; diff --git a/src/i18n/types.ts b/src/i18n/types.ts index ca8a70c961..ae9412ea4d 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -106,19 +106,11 @@ export type TDateTimeParserInput = string | number | Date; export type TDateTimeParserOutput = string | number | Date | Dayjs.Dayjs | Moment; export type TDateTimeParser = (input?: TDateTimeParserInput) => TDateTimeParserOutput; -export type SupportedTranslations = - | 'de' - | 'en' - | 'es' - | 'fr' - | 'hi' - | 'it' - | 'ja' - | 'ko' - | 'nl' - | 'pt' - | 'ru' - | 'tr'; +/** + * Languages with translations bundled in the SDK. English is the only one; any other + * language is supplied by the integrator via `Streami18n.registerTranslation()`. + */ +export type SupportedTranslations = 'en'; export type DateFormatterOptions = TimestampFormatterOptions & { formatDate?: MessageContextValue['formatDate']; diff --git a/src/i18n/utils.ts b/src/i18n/utils.ts index dd9f48b1a8..9282b9961c 100644 --- a/src/i18n/utils.ts +++ b/src/i18n/utils.ts @@ -257,19 +257,6 @@ export const defaultDateTimeParser = (input?: TDateTimeParserInput) => Dayjs(inp export const isLanguageSupported = ( language: string, ): language is SupportedTranslations => { - const translations = [ - 'de', - 'en', - 'es', - 'fr', - 'hi', - 'it', - 'ja', - 'ko', - 'nl', - 'pt', - 'ru', - 'tr', - ]; + const translations: string[] = ['en']; return translations.some((translation) => language === translation); }; From 82ae74940512a870d8c673d907781f34878b67ce Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Mon, 10 Aug 2026 10:02:29 +0200 Subject: [PATCH 02/19] feat(i18n): replace natural-language keys with namespaced, stable keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translation keys were the English copy itself, so 375 of 706 en.json entries were "X": "X" duplication, any copy edit silently orphaned every translation, and the same word in different contexts could not be disambiguated (worked around with an ad-hoc `aria/` prefix). Keys are now stable dotted identifiers with the English copy passed inline as i18next's `defaultValue`: t('messageComposer.sendButton.label', 'Send Message') This keeps call sites self-documenting, makes en.json generated output rather than a maintained file, and preserves the graceful degradation the natural-language keys gave for free — a key missing from a custom dictionary still renders English. - 759 rewrites across 166 files, driven by a reviewed old->new mapping committed at scripts/i18n-migration/key-map.json (603 entries). That file doubles as the migration table integrators need: `translationsForLanguage` / `registerTranslation` dictionaries keyed on the old strings must be renamed. - namespaces follow the source tree (message.*, messageComposer.*, poll.* …) so keys are predictable from the component; genuinely shared copy lives in common.* - the `aria/` pseudo-namespace is retired in favour of an `.ariaLabel` leaf, which is what made "Send" ambiguous. This also fixes aria-labels that were rendering the raw "aria/Pause recording" key to screen readers, and interpolation that never ran ("Audio position {{ elapsed }} of {{ duration }}") — see the snapshot updates. - formatter/plumbing keys (timestamp.*, duration.*, translationBuilderTopic.*) keep resolving from en.json and are preserved explicitly: their values are formatter expressions, not copy, so they carry no inline default. - add src/i18n/externalStrings.ts to map English strings emitted by stream-chat notifications onto stable keys, with raw passthrough for unrecognised ones. The withReasonFallback helper is gone so every notification key is a literal the extractor can see. - defaultTranslatorFunction (used before Streami18n initialises and outside ) returned the key verbatim, which with opaque keys would render "messageComposer.sendButton.label" in real UI. It now renders the inline default and interpolates. - tests: ~40 files stubbed `t` as an identity function, which returned English only because keys *were* English. They now share mockT from mock-builders, which mirrors i18next's defaultValue, plural and interpolation behaviour. Stubs inside vi.mock / vi.hoisted factories get an inlined copy, since a top-level import is in its TDZ there. Bundle: 419,172 -> 306,680 bytes gzip across the ESM tree, cumulative with the locale removal in the previous commit (-27%). Note: no TypeScript key union yet — `t` still accepts any string. Follows separately. --- i18next.config.ts | 12 +- scripts/i18n-migration/apply-key-map.mjs | 141 + scripts/i18n-migration/collect-failures.mjs | 35 + scripts/i18n-migration/extract-callsites.mjs | 134 + .../i18n-migration/fix-stale-templates.mjs | 60 + .../i18n-migration/fix-test-expectations.mjs | 88 + scripts/i18n-migration/fix-test-mocks.mjs | 162 ++ scripts/i18n-migration/generate-key-map.mjs | 487 ++++ scripts/i18n-migration/key-map.json | 2502 +++++++++++++++++ scripts/i18n-migration/normalize-test-t.mjs | 162 ++ src/a11y/accessibleLabel.ts | 8 +- .../AIStateIndicator/AIStateIndicator.tsx | 4 +- .../Accessibility/NotificationAnnouncer.tsx | 2 +- .../__tests__/NotificationAnnouncer.test.tsx | 12 +- .../useIncomingMessageAnnouncements.test.tsx | 26 +- .../useInteractionAnnouncements.test.tsx | 33 +- .../hooks/useIncomingMessageAnnouncements.ts | 25 +- .../hooks/useInteractionAnnouncements.ts | 118 +- .../Attachment/AttachmentActions.tsx | 14 +- src/components/Attachment/Geolocation.tsx | 40 +- src/components/Attachment/Giphy.tsx | 6 +- .../LinkPreview/UnableToRenderCard.tsx | 2 +- src/components/Attachment/ModalGallery.tsx | 21 +- .../Attachment/UnsupportedAttachment.tsx | 2 +- .../Attachment/VisibilityDisclaimer.tsx | 2 +- src/components/Attachment/VoiceRecording.tsx | 5 +- .../__tests__/AttachmentActions.test.tsx | 22 +- .../Attachment/__tests__/Audio.test.tsx | 6 +- .../Attachment/__tests__/Giphy.test.tsx | 28 +- .../__tests__/WaveProgressBar.test.tsx | 7 +- .../Attachment/components/DownloadButton.tsx | 4 +- .../__tests__/ProgressBar.test.tsx | 12 +- .../__tests__/WithAudioPlayback.test.tsx | 20 +- .../AudioPlayback/components/ProgressBar.tsx | 5 +- .../components/WaveProgressBar.tsx | 5 +- .../components/progressBarA11y.ts | 22 +- .../plugins/AudioPlayerNotificationsPlugin.ts | 23 +- .../AudioPlayerNotificationsPlugin.test.ts | 3 +- src/components/BaseImage/ImagePlaceholder.tsx | 5 +- src/components/Button/PlayButton.tsx | 6 +- src/components/Channel/Channel.tsx | 2 +- .../hooks/useChannelHeaderOnlineStatus.ts | 6 +- src/components/ChannelList/ChannelList.tsx | 2 +- .../ChannelList/ChannelListHeader.tsx | 4 +- .../__tests__/ChannelListHeader.test.tsx | 3 +- .../ChannelListItemActionButtons.defaults.tsx | 62 +- .../ChannelListItemActionButtons.tsx | 2 +- .../ChannelListItemTimestamp.tsx | 2 +- .../__tests__/ChannelListItemUI.test.tsx | 12 +- .../ChannelListItemUI.test.tsx.snap | 2 +- .../ChannelListItem/__tests__/utils.test.ts | 40 +- .../hooks/useChannelDisplayName.ts | 5 +- src/components/ChannelListItem/utils.a11y.ts | 61 +- src/components/ChannelListItem/utils.tsx | 81 +- ...eReportLostConnectionSystemNotification.ts | 5 +- .../DateSeparator/DateSeparator.tsx | 2 +- .../__tests__/DateSeparator.test.tsx | 10 +- .../Dialog/__tests__/ContextMenu.test.tsx | 4 +- src/components/Dialog/components/Callout.tsx | 5 +- .../Dialog/components/ContextMenu.tsx | 12 +- src/components/Dialog/components/Prompt.tsx | 4 +- src/components/Dialog/components/Viewer.tsx | 4 +- .../EmptyStateIndicator.tsx | 16 +- src/components/Form/NumericInput.tsx | 4 +- src/components/Form/SwitchField.tsx | 4 +- .../Form/__tests__/SwitchField.test.tsx | 6 +- src/components/Gallery/GalleryHeader.tsx | 10 +- src/components/Gallery/GalleryUI.tsx | 4 +- src/components/LoadMore/LoadMoreButton.tsx | 5 +- .../Loading/LoadingErrorIndicator.tsx | 8 +- .../__tests__/LoadingErrorIndicator.test.tsx | 2 +- .../Loading/progress-indicators.tsx | 6 +- .../Location/ShareLocationDialog.tsx | 28 +- .../AudioRecorderRecordingControls.tsx | 27 +- .../AudioRecordingButtonWithNotification.tsx | 5 +- .../AudioRecorder/AudioRecordingPlayback.tsx | 6 +- .../__snapshots__/AudioRecorder.test.tsx.snap | 44 +- .../RecordingPermissionDeniedNotification.tsx | 20 +- .../classes/MediaRecorderController.ts | 25 +- .../MessageAlsoSentInChannelIndicator.tsx | 8 +- src/components/Message/MessageBlocked.tsx | 2 +- .../Message/MessageDeletedBubble.tsx | 2 +- .../Message/MessageEditedIndicator.tsx | 2 +- .../Message/MessageRepliesCountButton.tsx | 6 +- src/components/Message/MessageStatus.tsx | 6 +- src/components/Message/MessageText.tsx | 6 +- .../Message/MessageTranslationIndicator.tsx | 16 +- src/components/Message/MessageUI.tsx | 2 +- src/components/Message/PinIndicator.tsx | 6 +- .../Message/ReminderNotification.tsx | 38 +- src/components/Message/Timestamp.tsx | 2 +- .../Message/__tests__/MessageDeleted.test.tsx | 3 +- .../Message/__tests__/MessageStatus.test.tsx | 3 +- .../Message/__tests__/MessageText.test.tsx | 12 +- .../__tests__/MessageTimestamp.test.tsx | 10 +- .../Message/__tests__/QuotedMessage.test.tsx | 8 +- .../Message/__tests__/utils.test.ts | 16 +- ...essageAlsoSentInChannelNavigation.test.tsx | 22 +- .../Message/hooks/useDeleteHandler.ts | 7 +- .../useMessageAlsoSentInChannelNavigation.ts | 2 +- .../Message/hooks/useMuteHandler.ts | 8 +- src/components/Message/hooks/usePinHandler.ts | 14 +- src/components/Message/utils.tsx | 36 +- .../MessageActions/DeleteMessageAlert.tsx | 14 +- .../MessageActions/DownloadSubmenu.tsx | 16 +- .../MessageActions.defaults.tsx | 148 +- .../MessageActions/MessageActions.tsx | 4 +- .../MessageActions/RemindMeSubmenu.tsx | 6 +- .../MessageBounce/MessageBouncePrompt.tsx | 14 +- .../AudioAttachmentPreview.tsx | 35 +- .../FileAttachmentPreview.tsx | 29 +- .../GeolocationPreview.tsx | 28 +- .../MediaAttachmentPreview.tsx | 2 +- .../UnsupportedAttachmentPreview.tsx | 2 +- .../utils/AttachmentPreviewRoot.tsx | 11 +- .../AttachmentSelector/AttachmentSelector.tsx | 25 +- .../AttachmentSelector/CommandsMenu.tsx | 33 +- .../__tests__/CommandsMenu.test.tsx | 4 +- .../MessageComposer/CommandChip.tsx | 6 +- .../MessageComposer/EditedMessagePreview.tsx | 2 +- .../MessageComposer/QuotedMessagePreview.tsx | 56 +- .../RemoveAttachmentPreviewButton.tsx | 5 +- src/components/MessageComposer/SendButton.tsx | 2 +- .../MessageComposer/SendToChannelCheckbox.tsx | 10 +- .../StopAIGenerationButton.tsx | 5 +- .../MessageComposer/WithDragAndDropUpload.tsx | 11 +- .../__tests__/AttachmentSelector.test.tsx | 8 +- .../__tests__/CommandChip.test.tsx | 21 +- .../__tests__/MessageInput.test.tsx | 38 +- .../useMessageComposerCommands.test.tsx | 8 +- .../MessageComposer/hooks/useSendMessageFn.ts | 5 +- .../hooks/useUpdateMessageFn.ts | 5 +- src/components/MessageComposer/icons.tsx | 2 +- .../MessageList/NewMessageNotification.tsx | 8 +- .../ScrollToLatestMessageButton.tsx | 5 +- .../UnreadMessagesNotification.tsx | 16 +- .../MessageList/UnreadMessagesSeparator.tsx | 16 +- .../ScrollToLatestMessageButton.test.tsx | 2 +- src/components/Notifications/Notification.tsx | 7 +- .../Notifications/NotificationList.tsx | 2 +- .../__tests__/Notification.test.tsx | 16 +- .../Poll/PollActions/AddCommentPrompt.tsx | 29 +- .../Poll/PollActions/EndPollAlert.tsx | 11 +- .../Poll/PollActions/PollActions.tsx | 18 +- .../Poll/PollActions/PollAnswerList.tsx | 12 +- .../Poll/PollActions/PollOptionsFullList.tsx | 7 +- .../Poll/PollActions/PollQuestion.tsx | 4 +- .../PollResults/PollOptionWithVotes.tsx | 2 +- .../PollResults/PollOptionWithVotesHeader.tsx | 10 +- .../PollActions/PollResults/PollResults.tsx | 16 +- .../PollActions/SuggestPollOptionPrompt.tsx | 29 +- .../__tests__/EndPollAlert.test.tsx | 7 +- .../MultipleAnswersField.tsx | 39 +- .../Poll/PollCreationDialog/NameField.tsx | 11 +- .../PollCreationDialog/OptionFieldSet.tsx | 40 +- .../PollCreationDialog/PollCreationDialog.tsx | 28 +- .../PollCreationDialogControls.tsx | 6 +- .../PollOptionReorderHandle.tsx | 23 +- src/components/Poll/PollHeader.tsx | 12 +- src/components/Poll/PollOptionList.tsx | 4 +- src/components/Poll/PollOptionSelector.tsx | 4 +- src/components/Poll/PollVote.tsx | 8 +- .../Poll/__tests__/AddCommentForm.test.tsx | 3 +- src/components/Poll/__tests__/Poll.test.tsx | 3 +- .../Poll/__tests__/PollActions.test.tsx | 13 +- .../Poll/__tests__/PollHeader.test.tsx | 5 +- .../Poll/__tests__/PollOptionList.test.tsx | 8 +- .../__tests__/SuggestPollOptionForm.test.tsx | 3 +- .../ReactFileUtilities/UploadButton.tsx | 2 +- src/components/Reactions/MessageReactions.tsx | 25 +- .../Reactions/MessageReactionsDetail.tsx | 37 +- src/components/Reactions/ReactionSelector.tsx | 29 +- .../Reactions/ReactionSelectorWithButton.tsx | 2 +- .../__tests__/MessageReactionsDetail.test.tsx | 11 +- .../__tests__/ReactionSelector.test.tsx | 12 +- .../ReactionSelectorWithButton.test.tsx | 22 +- .../Reactions/hooks/useFetchReactions.ts | 5 +- src/components/Search/SearchBar/SearchBar.tsx | 10 +- .../Search/SearchResults/SearchResultItem.tsx | 10 +- .../Search/SearchResults/SearchResults.tsx | 2 +- .../SearchResults/SearchResultsHeader.tsx | 19 +- .../SearchResults/SearchResultsPresearch.tsx | 2 +- .../SearchSourceResultListFooter.tsx | 2 +- .../SearchSourceResultsEmpty.tsx | 4 +- .../SearchSourceResultsLoadingIndicator.tsx | 10 +- .../Search/__tests__/Search.test.tsx | 5 +- .../Search/__tests__/SearchBar.test.tsx | 5 +- .../__tests__/SearchResultItem.test.tsx | 12 +- .../Search/__tests__/SearchResults.test.tsx | 5 +- .../__tests__/SearchResultsHeader.test.tsx | 46 +- .../SearchSourceResultListFooter.test.tsx | 12 +- .../useLatestMessagePreview.test.tsx | 6 +- .../hooks/useLatestMessagePreview.ts | 64 +- .../MentionItem/BroadcastMentionItem.tsx | 4 +- .../SuggestionList/MentionItem/RoleItem.tsx | 6 +- .../SuggestionList/SuggestionList.tsx | 17 +- .../TextareaComposer/TextareaComposer.tsx | 4 +- .../__tests__/CommandItem.test.tsx | 20 +- .../__tests__/MentionItem.test.tsx | 16 +- .../__tests__/SuggestionList.test.tsx | 34 +- .../hooks/useTextareaPlaceholder.ts | 24 +- src/components/Thread/ThreadHeader.tsx | 12 +- src/components/Thread/ThreadStart.tsx | 6 +- .../Thread/__tests__/ThreadHeader.test.tsx | 10 +- .../Thread/__tests__/ThreadStart.test.tsx | 11 +- .../Threads/ThreadList/ThreadList.tsx | 2 +- .../ThreadList/ThreadListEmptyPlaceholder.tsx | 2 +- .../Threads/ThreadList/ThreadListHeader.tsx | 4 +- .../Threads/ThreadList/ThreadListItemUI.tsx | 6 +- .../ThreadListUnseenThreadsBanner.tsx | 12 +- .../ThreadList/__tests__/ThreadList.test.tsx | 5 +- .../__tests__/ThreadListHeader.test.tsx | 3 +- .../__tests__/ThreadListItemUI.test.tsx | 12 +- .../ThreadList/__tests__/utils.a11y.test.ts | 16 +- .../Threads/ThreadList/utils.a11y.ts | 22 +- .../__tests__/TypingIndicator.test.tsx | 12 +- .../__tests__/getTypingStatusMessage.test.ts | 7 +- .../utils/getTypingStatusMessage.ts | 18 +- src/components/VideoPlayer/VideoThumbnail.tsx | 2 +- .../NotificationTranslationTopic.ts | 6 +- .../notifications/translators.ts | 104 +- .../translatorsByNotificationType.ts | 33 +- .../NotificationTranslationBuilder.test.ts | 84 +- src/i18n/__tests__/utils.test.ts | 63 +- src/i18n/en.json | 1257 +++++---- src/i18n/externalStrings.ts | 41 + src/i18n/utils.ts | 40 +- src/mock-builders/__test__/translator.test.ts | 56 +- src/mock-builders/context.ts | 3 +- src/mock-builders/translator.ts | 59 +- .../ChannelDetail/AvatarWithChannelDetail.tsx | 10 +- .../ChannelDetailSearchInput.tsx | 4 +- .../SectionNavigatorHeader.tsx | 5 +- .../__tests__/SectionNavigatorHeader.test.tsx | 22 +- .../ChannelFilesEmptyList.tsx | 7 +- .../ChannelFilesView/ChannelFilesView.tsx | 5 +- .../__tests__/ChannelFilesView.test.tsx | 3 +- .../ChannelManagementActions.defaults.tsx | 137 +- .../ChannelManagementView.tsx | 47 +- .../ChannelMediaEmptyList.tsx | 10 +- .../ChannelMediaView/ChannelMediaView.tsx | 28 +- .../__tests__/ChannelMediaView.test.tsx | 21 +- .../ChannelMemberActions.defaults.tsx | 118 +- .../ChannelMemberDetail.tsx | 24 +- .../__tests__/ChannelMemberDetail.test.tsx | 4 +- .../ChannelMembersAddView.tsx | 29 +- .../ChannelMembersBrowseView.tsx | 45 +- .../ChannelMembersHeaderActions.defaults.tsx | 21 +- .../ChannelMembersView/ChannelMembersView.tsx | 17 +- .../__tests__/ChannelMembersAddView.test.tsx | 20 +- .../ChannelMembersBrowseView.test.tsx | 7 +- ...nnelMembersHeaderActions.defaults.test.tsx | 3 +- .../__tests__/ChannelMembersView.test.tsx | 8 +- .../PinnedMessagesEmptyList.tsx | 10 +- .../PinnedMessagesView/PinnedMessagesView.tsx | 24 +- .../__tests__/PinnedMessagesView.test.tsx | 10 +- ...ChannelManagementActions.defaults.test.tsx | 28 +- .../__tests__/ChannelManagementView.test.tsx | 22 +- src/plugins/Emojis/EmojiPicker.tsx | 2 +- src/plugins/SlotLayout/ChatView.tsx | 20 +- .../__tests__/ChatViewNavigation.test.tsx | 3 +- .../__tests__/useSlotEntity.test.tsx | 3 +- 262 files changed, 7207 insertions(+), 1942 deletions(-) create mode 100644 scripts/i18n-migration/apply-key-map.mjs create mode 100644 scripts/i18n-migration/collect-failures.mjs create mode 100644 scripts/i18n-migration/extract-callsites.mjs create mode 100644 scripts/i18n-migration/fix-stale-templates.mjs create mode 100644 scripts/i18n-migration/fix-test-expectations.mjs create mode 100644 scripts/i18n-migration/fix-test-mocks.mjs create mode 100644 scripts/i18n-migration/generate-key-map.mjs create mode 100644 scripts/i18n-migration/key-map.json create mode 100644 scripts/i18n-migration/normalize-test-t.mjs create mode 100644 src/i18n/externalStrings.ts diff --git a/i18next.config.ts b/i18next.config.ts index 5c62471a8c..67dab6c2ac 100644 --- a/i18next.config.ts +++ b/i18next.config.ts @@ -14,10 +14,14 @@ export default defineConfig({ // `removeUnusedKeys` prunes anything the extractor cannot see in a `t()` call, so every // key resolved from a runtime value must be preserved explicitly here or it gets deleted. preservePatterns: [ - // Integrator-overridable timestamp format strings; never referenced as a literal. - 'timestamp/*', - // ISO language names, resolved via `t(languageKey)` in MessageTranslationIndicator. - 'language/*', + // Values are formatter expressions ("{{ timestamp | timestampFormatter(...) }}"), not + // English copy, so call sites deliberately pass no inline default. Without preserving + // them, extraction would overwrite each value with its own key name. + 'timestamp.*', + 'duration.*', + 'translationBuilderTopic.*', + // ISO language names, resolved via `t('language.' + code)` in MessageTranslationIndicator. + 'language.*', ], removeUnusedKeys: true, }, diff --git a/scripts/i18n-migration/apply-key-map.mjs b/scripts/i18n-migration/apply-key-map.mjs new file mode 100644 index 0000000000..f2894fb2df --- /dev/null +++ b/scripts/i18n-migration/apply-key-map.mjs @@ -0,0 +1,141 @@ +// Rewrites `t('Natural text', …)` into `t('namespaced.key', 'Natural text', …)` using the +// reviewed mapping. Edits are collected with AST positions and applied right-to-left so +// offsets stay valid. Prettier normalises quoting afterwards. +// +// Non-literal call forms (t(cond ? 'a' : 'b'), t(x || 'a'), and the notification-translator +// option objects) are reported and left alone — they are handled by hand. +import ts from 'typescript'; +import fs from 'node:fs'; +import path from 'node:path'; + +const DRY = process.argv.includes('--dry'); +const mapping = JSON.parse( + fs.readFileSync('scripts/i18n-migration/key-map.json', 'utf8'), +).keys; +const en = JSON.parse(fs.readFileSync('src/i18n/en.json', 'utf8')); + +const files = []; +(function walk(dir) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) { + if (e.name === '__tests__' || e.name === 'mock-builders') continue; + walk(p); + } else if (/\.tsx?$/.test(e.name) && !e.name.endsWith('.d.ts')) files.push(p); + } +})('src'); + +// The inline default is the English *value*, not the key: `aria/Send` renders as "Send". +const defaultFor = (oldKey) => en[oldKey] ?? oldKey.replace(/^aria\//, ''); +const pluralDefaults = (oldKey) => ({ + one: en[`${oldKey}_one`], + other: en[`${oldKey}_other`], +}); + +const lit = (s) => JSON.stringify(s); + +const isTCallee = (expr) => + (ts.isIdentifier(expr) && expr.text === 't') || + (ts.isPropertyAccessExpression(expr) && expr.name.text === 't'); + +let rewritten = 0; +let skipped = []; +const touchedFiles = []; + +for (const file of files) { + const text = fs.readFileSync(file, 'utf8'); + const sf = ts.createSourceFile( + file, + text, + ts.ScriptTarget.Latest, + true, + file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + + const edits = []; + + const visit = (node) => { + if (ts.isCallExpression(node) && isTCallee(node.expression)) { + const [arg0, ...rest] = node.arguments; + if (arg0 && ts.isStringLiteralLike(arg0)) { + const oldKey = arg0.text; + const entry = mapping[oldKey]; + if (entry) { + const newKey = entry.key; + if (!entry.prose) { + // Formatter/plumbing key: swap the key, leave everything else alone. + edits.push({ + start: arg0.getStart(sf), + end: arg0.getEnd(), + text: lit(newKey), + }); + } else if (entry.plural) { + const { one, other } = pluralDefaults(oldKey); + if (one === undefined || other === undefined) { + skipped.push({ + file, + key: oldKey, + why: 'plural defaults missing from en.json', + }); + } else { + const defaults = `defaultValue_one: ${lit(one)}, defaultValue_other: ${lit(other)}`; + const opts = rest[0]; + if (opts && ts.isObjectLiteralExpression(opts)) { + // Merge the defaults into the existing options object. + edits.push({ + start: arg0.getStart(sf), + end: arg0.getEnd(), + text: lit(newKey), + }); + const inner = opts.properties.length + ? `${opts.properties.map((p) => p.getText(sf)).join(', ')}, ${defaults}` + : defaults; + edits.push({ + start: opts.getStart(sf), + end: opts.getEnd(), + text: `{ ${inner} }`, + }); + } else if (!opts) { + skipped.push({ + file, + key: oldKey, + why: 'plural key called without options', + }); + } else { + skipped.push({ + file, + key: oldKey, + why: 'plural options not an object literal', + }); + } + } + } else { + // Singular prose: insert the English copy as a positional defaultValue. + edits.push({ + start: arg0.getStart(sf), + end: arg0.getEnd(), + text: `${lit(newKey)}, ${lit(defaultFor(oldKey))}`, + }); + } + } else { + skipped.push({ file, key: oldKey, why: 'no mapping entry' }); + } + } + } + ts.forEachChild(node, visit); + }; + visit(sf); + + if (!edits.length) continue; + edits.sort((a, b) => b.start - a.start); + let out = text; + for (const e of edits) out = out.slice(0, e.start) + e.text + out.slice(e.end); + rewritten += edits.length; + touchedFiles.push(file); + if (!DRY) fs.writeFileSync(file, out); +} + +console.log(DRY ? '(dry run)' : '(applied)'); +console.log('edits:', rewritten, 'in', touchedFiles.length, 'files'); +console.log('skipped:', skipped.length); +for (const s of skipped) console.log(` ${s.file} ${JSON.stringify(s.key)} — ${s.why}`); diff --git a/scripts/i18n-migration/collect-failures.mjs b/scripts/i18n-migration/collect-failures.mjs new file mode 100644 index 0000000000..7682ee6726 --- /dev/null +++ b/scripts/i18n-migration/collect-failures.mjs @@ -0,0 +1,35 @@ +// Reads a vitest JSON report and pairs each failing assertion's Expected/Received strings, so +// the "test asserted the uninterpolated template" failures can be reviewed and fixed in bulk. +import fs from 'node:fs'; + +const report = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); +const ANSI = /\[[0-9;]*m/g; +const cwd = process.cwd() + '/'; + +const pairs = []; +for (const file of report.testResults ?? []) { + for (const assertion of file.assertionResults ?? []) { + if (assertion.status !== 'failed') continue; + const msg = (assertion.failureMessages ?? []).join('\n').replace(ANSI, ''); + const exp = msg.match( + /Expected(?: the element to have attribute)?:\s*\n?\s*(?:[\w-]+=)?"([\s\S]*?)"\s*\n/, + ); + const rec = msg.match(/Received:\s*\n?\s*(?:[\w-]+=)?"([\s\S]*?)"\s*\n/); + if (exp && rec && exp[1] !== rec[1]) { + pairs.push({ + file: file.name.replace(cwd, ''), + title: assertion.fullName, + expected: exp[1], + received: rec[1], + }); + } + } +} + +fs.writeFileSync(process.argv[3], JSON.stringify(pairs, null, 2) + '\n'); +console.log('pairs:', pairs.length); +for (const p of pairs) { + console.log(` ${p.file.split('/').pop()}`); + console.log(` - ${JSON.stringify(p.expected)}`); + console.log(` + ${JSON.stringify(p.received)}`); +} diff --git a/scripts/i18n-migration/extract-callsites.mjs b/scripts/i18n-migration/extract-callsites.mjs new file mode 100644 index 0000000000..5477d91bcf --- /dev/null +++ b/scripts/i18n-migration/extract-callsites.mjs @@ -0,0 +1,134 @@ +// Collects every translation-key string literal reachable from a `t(...)` call (or from the +// notification-translator option objects), together with the context needed to name it. +import ts from 'typescript'; +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = process.argv[2] ?? 'src'; +const OUT = process.argv[3]; + +const files = []; +(function walk(dir) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) { + if (e.name === '__tests__' || e.name === 'mock-builders') continue; + walk(p); + } else if (/\.tsx?$/.test(e.name) && !e.name.endsWith('.d.ts')) files.push(p); + } +})(ROOT); + +// Option properties in the notification translators whose value is a translation key. +const KEY_PROPS = new Set(['fallbackTranslationKey', 'reasonTranslationKey']); + +const records = []; + +const isTCallee = (expr) => { + if (ts.isIdentifier(expr)) return expr.text === 't'; + if (ts.isPropertyAccessExpression(expr)) return expr.name.text === 't'; + return false; +}; + +// Walk up to find the JSX attribute or object property this call sits inside, which is the +// best available signal for the modality (aria-label -> ariaLabel, placeholder, title...). +const contextOf = (node) => { + let cur = node.parent; + let depth = 0; + while (cur && depth < 6) { + if (ts.isJsxAttribute(cur)) return { kind: 'jsxAttr', name: cur.name.getText() }; + if ( + ts.isPropertyAssignment(cur) && + (ts.isIdentifier(cur.name) || ts.isStringLiteral(cur.name)) + ) + return { kind: 'prop', name: cur.name.text }; + if (ts.isJsxExpression(cur) && cur.parent && ts.isJsxElement(cur.parent)) + return { kind: 'jsxChild', name: 'text' }; + cur = cur.parent; + depth++; + } + return { kind: 'none', name: null }; +}; + +const interpolationsOf = (s) => + [...s.matchAll(/\{\{\s*([\w.]+)\s*(?:,[^}]*)?\}\}/g)].map((m) => m[1]); + +for (const file of files) { + const text = fs.readFileSync(file, 'utf8'); + // Parsing a .ts file as TSX makes the parser read `Foo` type arguments as JSX and + // silently yield a garbage tree, so the script kind has to follow the extension. + const sf = ts.createSourceFile( + file, + text, + ts.ScriptTarget.Latest, + true, + file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + + const record = (keyNode, form) => { + if (!ts.isStringLiteralLike(keyNode)) return false; + const key = keyNode.text; + const { line } = sf.getLineAndCharacterOfPosition(keyNode.getStart(sf)); + const ctx = contextOf(keyNode); + records.push({ + file, + line: line + 1, + key, + form, + ctxKind: ctx.kind, + ctxName: ctx.name, + interpolations: interpolationsOf(key), + start: keyNode.getStart(sf), + end: keyNode.getEnd(), + }); + return true; + }; + + // Unwrap `a ? 'x' : 'y'` and `v || 'x'` so both branches are captured. + const recordKeyExpr = (expr, form) => { + if (!expr) return; + if (ts.isStringLiteralLike(expr)) return void record(expr, form); + if (ts.isConditionalExpression(expr)) { + recordKeyExpr(expr.whenTrue, 'conditional'); + recordKeyExpr(expr.whenFalse, 'conditional'); + return; + } + if (ts.isBinaryExpression(expr)) { + const op = expr.operatorToken.kind; + if ( + op === ts.SyntaxKind.BarBarToken || + op === ts.SyntaxKind.QuestionQuestionToken + ) { + recordKeyExpr(expr.left, 'fallback'); + recordKeyExpr(expr.right, 'fallback'); + } + return; + } + if (ts.isParenthesizedExpression(expr)) return recordKeyExpr(expr.expression, form); + // t(someVariable) — a runtime key; nothing to rename here. + }; + + const visit = (node) => { + if (ts.isCallExpression(node) && isTCallee(node.expression)) { + recordKeyExpr(node.arguments[0], 'literal'); + } + if ( + ts.isPropertyAssignment(node) && + (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) && + KEY_PROPS.has(node.name.text) + ) { + record(node.initializer, 'optionProp'); + } + ts.forEachChild(node, visit); + }; + visit(sf); +} + +const out = { generatedFrom: ROOT, count: records.length, records }; +if (OUT) fs.writeFileSync(OUT, JSON.stringify(out, null, 2)); + +const byForm = {}; +for (const r of records) byForm[r.form] = (byForm[r.form] ?? 0) + 1; +console.log('call sites:', records.length); +console.log('distinct keys:', new Set(records.map((r) => r.key)).size); +console.log('files:', new Set(records.map((r) => r.file)).size); +console.log('by form:', byForm); diff --git a/scripts/i18n-migration/fix-stale-templates.mjs b/scripts/i18n-migration/fix-stale-templates.mjs new file mode 100644 index 0000000000..d1b924f7bf --- /dev/null +++ b/scripts/i18n-migration/fix-stale-templates.mjs @@ -0,0 +1,60 @@ +// Updates assertions that still expect an *uninterpolated* translation template +// ("{{ typing }} is typing") to the interpolated copy the component now renders +// ("jessica is typing"). Only pairs whose expected value still contains `{{ … }}` are +// touched, so genuine mismatches are left to fail. +import fs from 'node:fs'; + +const report = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); +const APPLY = process.argv.includes('--apply'); +const ANSI = /\u001b\[[0-9;]*m/g; +const cwd = process.cwd() + '/'; + +const pairs = []; +for (const file of report.testResults ?? []) { + for (const assertion of file.assertionResults ?? []) { + if (assertion.status !== 'failed') continue; + const msg = (assertion.failureMessages ?? []).join('\n').replace(ANSI, ''); + // `toHaveTextContent` / `toHaveAttribute` / `toBe` all render an Expected/Received block. + const m = msg.match( + /Expected(?: element to have text content| the element to have attribute)?:\s*\n\s*(?:[\w-]+=)?"?([\s\S]*?)"?\s*\nReceived:\s*\n\s*(?:[\w-]+=)?"?([\s\S]*?)"?\s*(?:\n|$)/, + ); + if (!m) continue; + const [, expected, received] = m; + if (!expected.includes('{{') || expected === received) continue; + pairs.push({ file: file.name.replace(cwd, ''), expected, received }); + } +} + +// De-duplicate: the same template often appears in several assertions. +const seen = new Set(); +const unique = pairs.filter((p) => { + const k = `${p.file}::${p.expected}`; + if (seen.has(k)) return false; + seen.add(k); + return true; +}); + +console.log(APPLY ? '(applying)' : '(dry run)', unique.length, 'stale templates'); +let applied = 0; +for (const p of unique) { + console.log(` ${p.file.split('/').pop()}`); + console.log(` - ${JSON.stringify(p.expected)}`); + console.log(` + ${JSON.stringify(p.received)}`); + if (!APPLY) continue; + const text = fs.readFileSync(p.file, 'utf8'); + const needle = JSON.stringify(p.expected).slice(1, -1); // escaped body, quote-agnostic + let out = text; + for (const q of ["'", '"', '`']) { + const from = q + p.expected + q; + if (out.includes(from)) { + out = out.split(from).join(q + p.received + q); + } + } + if (out !== text) { + fs.writeFileSync(p.file, out); + applied++; + } else { + console.log(` ! literal not found verbatim (needle: ${needle.slice(0, 40)})`); + } +} +if (APPLY) console.log('files rewritten:', applied); diff --git a/scripts/i18n-migration/fix-test-expectations.mjs b/scripts/i18n-migration/fix-test-expectations.mjs new file mode 100644 index 0000000000..03da4520a9 --- /dev/null +++ b/scripts/i18n-migration/fix-test-expectations.mjs @@ -0,0 +1,88 @@ +// Some tests assert on the *old* translation key because the identity `t` mock made the key and +// the rendered text identical (`'aria/Send'` rendered as "aria/Send"). Now that keys are opaque +// and the inline default renders, those assertions must use the English copy instead. +// +// Only literals that are an old key AND differ from their English value are touched, so +// assertions like `'Cancel'` (where key === value) are left alone. +import ts from 'typescript'; +import fs from 'node:fs'; +import path from 'node:path'; + +const DRY = process.argv.includes('--dry'); +const mapping = JSON.parse( + fs.readFileSync('scripts/i18n-migration/key-map.json', 'utf8'), +).keys; +// English copy, taken from the pre-migration en.json (keyed by the old natural-language keys). +const originalEn = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); + +const englishFor = (oldKey) => { + if (originalEn[oldKey] !== undefined) return originalEn[oldKey]; + // plural bases have no bare entry; prefer the `_other` form for assertions + if (originalEn[`${oldKey}_other`] !== undefined) return originalEn[`${oldKey}_other`]; + if (oldKey.startsWith('aria/')) return oldKey.slice('aria/'.length); + return undefined; +}; + +const files = []; +(function walk(dir) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (/\.test\.tsx?$/.test(e.name)) files.push(p); + } +})('src'); + +let total = 0; +const changedFiles = []; +const samples = []; + +for (const file of files) { + const text = fs.readFileSync(file, 'utf8'); + const sf = ts.createSourceFile( + file, + text, + ts.ScriptTarget.Latest, + true, + file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + + const edits = []; + (function visit(node) { + if (ts.isStringLiteralLike(node)) { + const oldKey = node.text; + const entry = mapping[oldKey]; + if (entry) { + // Formatter/plumbing keys are passed *as keys* in tests (timestampTranslationKey + // props, postProcessor names), so they get the new key. Prose keys were being used as + // stand-ins for the rendered text, so they get the English copy. + const replacement = entry.prose ? englishFor(oldKey) : entry.key; + if (replacement !== undefined && replacement !== oldKey) { + edits.push({ + start: node.getStart(sf), + end: node.getEnd(), + text: JSON.stringify(replacement), + from: oldKey, + to: replacement, + }); + } + } + } + ts.forEachChild(node, visit); + })(sf); + + if (!edits.length) continue; + edits.sort((a, b) => b.start - a.start); + let out = text; + for (const e of edits) out = out.slice(0, e.start) + e.text + out.slice(e.end); + total += edits.length; + changedFiles.push(file); + if (samples.length < 12) + samples.push(...edits.slice(0, 2).map((e) => [file, e.from, e.to])); + if (!DRY) fs.writeFileSync(file, out); +} + +console.log(DRY ? '(dry run)' : '(applied)'); +console.log('replacements:', total, 'in', changedFiles.length, 'files'); +for (const [f, from, to] of samples) { + console.log(` ${path.basename(f)}: ${JSON.stringify(from)} -> ${JSON.stringify(to)}`); +} diff --git a/scripts/i18n-migration/fix-test-mocks.mjs b/scripts/i18n-migration/fix-test-mocks.mjs new file mode 100644 index 0000000000..bb80ed4951 --- /dev/null +++ b/scripts/i18n-migration/fix-test-mocks.mjs @@ -0,0 +1,162 @@ +// Tests stub `t` as an identity function. That returned English while keys *were* English; +// now it returns the dotted key. This rewrites those stubs to honour the inline `defaultValue` +// the components pass, so the existing assertions on English copy keep working. +// +// Two output forms, chosen by position: +// - inside a hoisted `vi.mock(...)` factory -> an inline function (a top-level import would +// be in its TDZ when the hoisted factory runs) +// - anywhere else -> the shared `mockT` from mock-builders +import ts from 'typescript'; +import fs from 'node:fs'; +import path from 'node:path'; + +const INLINE = + '(key: string, defaultValue?: unknown) =>\n' + + " typeof defaultValue === 'string' ? defaultValue : key"; + +const files = []; +(function walk(dir) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (/\.test\.tsx?$/.test(e.name)) files.push(p); + } +})('src'); + +const relativeImport = (from) => { + const rel = path + .relative(path.dirname(from), 'src/mock-builders/translator') + .replace(/\\/g, '/'); + return rel.startsWith('.') ? rel : `./${rel}`; +}; + +// An arrow function whose body is exactly its single parameter: `(k) => k`, `(k: string) => k`. +const isIdentityArrow = (node, sf) => { + if (!ts.isArrowFunction(node)) return false; + if (node.parameters.length !== 1) return false; + const param = node.parameters[0]; + if (!ts.isIdentifier(param.name)) return false; + const body = node.body; + if (ts.isIdentifier(body)) return body.text === param.name.text; + // `(k) => k.replace(/^aria\//, '')` and `(k) => k.split('/').pop()` are prefix-strippers for + // the old `aria/` namespace: obsolete, and equivalent to identity for dotted keys. + const src = body.getText(sf); + return ( + /^\w+\.replace\(\/\^aria\\\/\/,\s*''\)$/.test(src) || + /^\w+\.split\('\/'\)\.pop\(\)$/.test(src) + ); +}; + +let changed = 0; +let inlineCount = 0; +let importCount = 0; + +for (const file of files) { + const text = fs.readFileSync(file, 'utf8'); + const sf = ts.createSourceFile( + file, + text, + ts.ScriptTarget.Latest, + true, + file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + + // Ranges covered by hoisted vi.mock factories. + const hoisted = []; + (function collect(node) { + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === 'vi' && + (node.expression.name.text === 'mock' || node.expression.name.text === 'hoisted') + ) { + hoisted.push([node.getStart(sf), node.getEnd()]); + } + ts.forEachChild(node, collect); + })(sf); + const isHoisted = (pos) => hoisted.some(([s, e]) => pos >= s && pos < e); + + // Find identity arrows bound to a `t` property or a `t` variable. + const edits = []; + (function visit(node) { + let target = null; + if ( + ts.isPropertyAssignment(node) && + ts.isIdentifier(node.name) && + node.name.text === 't' + ) { + target = node.initializer; + } else if ( + ts.isVariableDeclaration(node) && + ts.isIdentifier(node.name) && + node.name.text === 't' && + node.initializer + ) { + target = node.initializer; + } + + if (target) { + // Unwrap `(...) as SomeType`, `((...))` and `vi.fn(...)`. + let inner = target; + let wrapVi = false; + for (;;) { + if (ts.isAsExpression(inner) || ts.isParenthesizedExpression(inner)) { + inner = inner.expression; + continue; + } + if ( + ts.isCallExpression(inner) && + ts.isPropertyAccessExpression(inner.expression) && + ts.isIdentifier(inner.expression.expression) && + inner.expression.expression.text === 'vi' && + inner.expression.name.text === 'fn' && + inner.arguments.length === 1 + ) { + wrapVi = true; + inner = inner.arguments[0]; + continue; + } + break; + } + + if (isIdentityArrow(inner, sf)) { + const useInline = isHoisted(inner.getStart(sf)); + const replacement = useInline ? INLINE : 'mockT'; + if (useInline) inlineCount++; + else importCount++; + edits.push({ + start: inner.getStart(sf), + end: inner.getEnd(), + text: wrapVi ? replacement : replacement, + needsImport: !useInline, + }); + } + } + ts.forEachChild(node, visit); + })(sf); + + if (!edits.length) continue; + + edits.sort((a, b) => b.start - a.start); + let out = text; + for (const e of edits) out = out.slice(0, e.start) + e.text + out.slice(e.end); + + if ( + edits.some((e) => e.needsImport) && + !/from\s+['"][^'"]*mock-builders\/translator['"]/.test(out) + ) { + const stmt = `import { mockT } from '${relativeImport(file)}';`; + const lastImport = [...out.matchAll(/^import .*?;$/gms)].pop(); + out = lastImport + ? `${out.slice(0, lastImport.index + lastImport[0].length)}\n${stmt}${out.slice(lastImport.index + lastImport[0].length)}` + : `${stmt}\n${out}`; + } + + fs.writeFileSync(file, out); + changed++; +} + +console.log('files updated:', changed); +console.log(' -> shared mockT:', importCount); +console.log(' -> inlined (inside vi.mock):', inlineCount); diff --git a/scripts/i18n-migration/generate-key-map.mjs b/scripts/i18n-migration/generate-key-map.mjs new file mode 100644 index 0000000000..cef33f518b --- /dev/null +++ b/scripts/i18n-migration/generate-key-map.mjs @@ -0,0 +1,487 @@ +// Produces a draft old-key -> new-key mapping for the natural-language -> namespaced-key +// migration. The draft is meant to be reviewed by hand; the collision and review reports it +// prints are the parts that need human judgment. +import fs from 'node:fs'; + +const CALLSITES = process.argv[2]; +const OUT = process.argv[3]; + +const callsites = JSON.parse(fs.readFileSync(CALLSITES, 'utf8')); +const en = JSON.parse(fs.readFileSync('src/i18n/en.json', 'utf8')); + +// --------------------------------------------------------------------------------------- +// Namespaces follow the source tree so that a dev editing a component can predict its keys. +// --------------------------------------------------------------------------------------- +const NAMESPACES = { + 'plugins/ChannelDetail': 'channelDetail', + 'plugins/Emojis': 'emojiPicker', + 'plugins/SlotLayout': 'slotLayout', + 'components/AIStateIndicator': 'aiState', + 'components/Accessibility': 'a11y', + 'components/Attachment': 'attachment', + 'components/AudioPlayback': 'audioPlayback', + 'components/BaseImage': 'baseImage', + 'components/Button': 'button', + 'components/Channel': 'channel', + 'components/ChannelHeader': 'channelHeader', + 'components/ChannelList': 'channelList', + 'components/ChannelListItem': 'channelListItem', + 'components/Chat': 'chat', + 'components/Dialog': 'dialog', + 'components/EmptyStateIndicator': 'emptyState', + 'components/Form': 'form', + 'components/Gallery': 'gallery', + 'components/LoadMore': 'loadMore', + 'components/Loading': 'loading', + 'components/Location': 'location', + 'components/MediaRecorder': 'mediaRecorder', + 'components/Message': 'message', + 'components/MessageActions': 'messageActions', + 'components/MessageBounce': 'messageBounce', + 'components/MessageComposer': 'messageComposer', + 'components/MessageList': 'messageList', + 'components/Notifications': 'notification', + 'components/Poll': 'poll', + 'components/ReactFileUtilities': 'fileUpload', + 'components/Reactions': 'reactions', + 'components/Search': 'search', + 'components/SummarizedMessagePreview': 'messagePreview', + 'components/TextareaComposer': 'textareaComposer', + 'components/Thread': 'thread', + 'components/Threads': 'threadList', + 'components/TypingIndicator': 'typing', + 'components/VideoPlayer': 'videoPlayer', + 'src/a11y': 'a11y', + 'src/i18n': 'notification', +}; + +// Keys resolved from a runtime value, or whose value is a formatter expression rather than +// prose. These get mechanical renames and must keep resolving from en.json (no inline default). +const MECHANICAL = [ + { re: /^language\/(.+)$/, to: (m) => `language.${m[1]}`, prose: true }, + // timestamp/relative* are real copy ("Today", "{{count}}d ago"); the PascalCase entries are + // formatter expressions and must keep resolving from en.json. + { re: /^timestamp\/(relative.+)$/, to: (m) => `timestamp.${m[1]}`, prose: true }, + { re: /^timestamp\/(.+)$/, to: (m) => `timestamp.${m[1]}`, prose: false }, + { re: /^duration\/(.+)$/, to: (m) => `duration.${camel(m[1])}`, prose: false }, + { + re: /^translationBuilderTopic\/(.+)$/, + to: (m) => `translationBuilderTopic.${m[1]}`, + prose: false, + }, + { + re: /^(\w+)-command-(args|description)$/, + to: (m) => `command.${m[1]}.${m[2]}`, + prose: true, + }, + { + re: /^search-results-header-filter-source-button-label--(\w+)$/, + to: (m) => `search.resultsHeader.filterSource.${m[1]}`, + prose: true, + }, + { + re: /^mention\/(\w+) Description$/, + to: (m) => `mention.${m[1].toLowerCase()}.description`, + prose: true, + }, + { + re: /^placeholder\/(.+)$/, + to: (m) => `poll.${camel(m[1])}.placeholder`, + prose: true, + }, + { + re: /^ThreadListUnseenThreadsBanner\/(.+)$/, + to: (m) => `threadList.unseenBanner.${camel(m[1])}`, + prose: true, + }, +]; + +// Split on whitespace/punctuation *and* on case boundaries, so an already-camel or Pascal +// identifier ("ChannelHeaderOnlineStatus") yields real words instead of one lowercased blob. +// Hand-authored names for keys where a copy-derived leaf reads badly. The notification set is +// named after the `translatorsByNotificationType` keys rather than the English sentence, so the +// key survives copy edits and lines up with the notification type it renders. +const OVERRIDES = { + // notifications (see src/i18n/TranslationBuilder/notifications/) + 'Error uploading attachment': 'notification.attachmentUploadFailed', + 'Attachment upload failed due to {{reason}}': + 'notification.attachmentUploadFailedWithReason', + 'Attachment upload blocked due to {{reason}}': + 'notification.attachmentUploadBlockedWithReason', + 'File is required for upload attachment': 'notification.attachmentFileMissing', + 'Local upload attachment missing local id': 'notification.attachmentIdMissing', + 'Wait until all attachments have uploaded': 'notification.attachmentUploadInProgress', + 'Missing permissions to upload the attachment': + 'notification.attachmentUploadForbidden', + 'Failed to create the poll': 'notification.pollCreateFailed', + 'Failed to create the poll due to {{reason}}': + 'notification.pollCreateFailedWithReason', + 'Failed to end the poll': 'notification.pollEndFailed', + 'Failed to end the poll due to {{reason}}': 'notification.pollEndFailedWithReason', + 'Poll ended': 'notification.pollEndSuccess', + 'Reached the vote limit. Remove an existing vote first.': 'notification.pollVoteLimit', + 'Failed to share location': 'notification.locationShareFailed', + 'Failed to retrieve location': 'notification.locationGetFailed', + 'Thread has not been found': 'notification.replySearchFailed', + 'Failed to jump to the first unread message': 'notification.jumpToFirstUnreadFailed', + 'Error reproducing the recording': 'notification.audioPlaybackError', + 'Command not available': 'notification.commandDisabled', + 'Command not available while editing': 'notification.commandDisabledWhileEditing', + 'Command not available while replying': 'notification.commandDisabledWhileReplying', + // reason values interpolated into the messages above + 'unsupported file type': 'notification.reason.unsupportedFileType', + 'size limit': 'notification.reason.sizeLimit', + 'unknown error': 'notification.reason.unknownError', + + // typing status: one key per arity (see getTypingStatusMessage) + '{{ typing }} is typing': 'typing.singleUser', + '{{ typing }} are typing': 'typing.twoUsers', + '{{ count }} people are typing': 'typing.manyUsers', + + // two distinct recorder failures whose copy differs only in the tail + 'An error has occurred during recording': 'mediaRecorder.error.recording', + 'An error has occurred during the recording processing': + 'mediaRecorder.error.processing', + 'Error starting recording': 'mediaRecorder.error.start', + + // generic vs counted attachment announcements + 'aria/Attachment': 'channelListItem.attachment.ariaLabel', + 'aria/{{ count }} attachment': 'channelListItem.attachmentCount.ariaLabel', + + // the copy is entirely interpolation, so a copy-derived leaf says nothing + 'aria/{{ count }} {{ suggestionsLabel }}': + 'a11y.interactionAnnouncements.suggestionsWithLabel.ariaLabel', + + // microphone/camera permission prompts: heading + body pairs + 'Allow access to microphone': 'mediaRecorder.permissionDenied.microphone.heading', + 'To start recording, allow the microphone access in your browser': + 'mediaRecorder.permissionDenied.microphone.body', + 'Allow access to camera': 'mediaRecorder.permissionDenied.camera.heading', + 'To start recording, allow the camera access in your browser': + 'mediaRecorder.permissionDenied.camera.body', +}; + +function words(s) { + return String(s) + .replace(/\{\{[^}]*\}\}/g, ' ') // drop interpolation placeholders + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // fooBar -> foo Bar + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // AIState -> AI State + .replace(/[^\p{L}\p{N}]+/gu, ' ') // drop punctuation/emoji + .trim() + .split(/\s+/) + .filter(Boolean); +} + +// Filler words carry no meaning in an identifier and eat the word budget. Negations are +// deliberately NOT listed: dropping them inverts the meaning of the name. +const STOPWORDS = new Set([ + 'a', + 'an', + 'the', + 'to', + 'in', + 'into', + 'of', + 'for', + 'your', + 'you', + 'this', + 'that', + 'these', + 'those', + 'and', + 'or', + 'is', + 'are', + 'be', + 'been', + 'will', + 'with', + 'from', + 'at', + 'on', + 'by', + 'it', + 'its', + 'has', + 'have', + 'was', + 'were', + 'do', + 'does', + 'as', + 'so', + 'if', + 'then', + 'there', + 'here', + 'all', + 'any', + 'my', +]); + +function camel(s, maxWords = 4) { + let w = words(s); + if (!w.length) return 'text'; + // Keep stopwords only if dropping them would leave nothing. + const trimmed = w.filter((x) => !STOPWORDS.has(x.toLowerCase())); + if (trimmed.length) w = trimmed; + return w + .slice(0, maxWords) + .map((x, i) => { + const lower = x.toLowerCase(); + return i === 0 ? lower : lower[0].toUpperCase() + lower.slice(1); + }) + .join(''); +} + +// The component segment: the file's own name, unless the file is a helper (utils/index/ +// *.defaults/*.a11y), in which case the containing directory is more meaningful. +function componentSegment(file) { + const parts = file.replace(/\.tsx?$/, '').split('/'); + let base = parts[parts.length - 1]; + const helper = /^(index|utils|utils\.a11y|constants|types|hooks)$/i.test(base); + base = base.replace(/\.(defaults|a11y)$/i, ''); + if (helper) base = parts[parts.length - 2] ?? base; + // hooks live in a hooks/ dir: use the hook name minus the `use` prefix + base = base.replace(/^use([A-Z])/, (_, c) => c.toLowerCase()); + // Component file names are often long compounds (AudioPlayerNotificationsPlugin); three + // words is enough to identify the component within its namespace. + return camel(base, 3); +} + +function namespaceOf(file) { + const m = file.match(/^src\/(components|plugins)\/([^/]+)/); + if (m) return NAMESPACES[`${m[1]}/${m[2]}`] ?? camel(m[2]); + const dir = file.split('/').slice(0, 2).join('/'); + return NAMESPACES[dir] ?? camel(dir.split('/').pop()); +} + +// Modality suffix, from the JSX attribute / object property the call sits in. +function suffixOf(rec, isAria) { + const n = (rec.ctxName ?? '').toLowerCase(); + if (isAria) return n.includes('describedby') ? 'description' : 'ariaLabel'; + if (n === 'aria-label' || n === 'arialabel') return 'ariaLabel'; + if (n === 'placeholder') return 'placeholder'; + if (n === 'title') return 'title'; + if (n === 'description') return 'description'; + if (n === 'heading') return 'heading'; + if (n === 'tooltip') return 'tooltip'; + if (n === 'message' || n === 'text') return 'text'; + return 'label'; +} + +const bare = (k) => k.replace(/_(one|other|zero|two|few|many)$/, ''); +const pluralBases = new Set( + Object.keys(en) + .filter((k) => /_(one|other|zero|two|few|many)$/.test(k)) + .map(bare), +); + +// --------------------------------------------------------------------------------------- +// Build the draft. +// --------------------------------------------------------------------------------------- +// One old key can appear at several call sites; pick the first (files are walked in a stable +// order) and record the rest so review can spot keys shared across unrelated components. +const byKey = new Map(); +for (const r of callsites.records) { + if (!byKey.has(r.key)) byKey.set(r.key, []); + byKey.get(r.key).push(r); +} + +const map = {}; +const meta = {}; +const parts = {}; +const review = []; + +for (const [key, recs] of byKey) { + if (OVERRIDES[key]) { + map[key] = OVERRIDES[key]; + meta[key] = { + prose: true, + override: true, + sites: recs.length, + plural: pluralBases.has(key), + interpolations: recs[0].interpolations, + files: [...new Set(recs.map((x) => x.file))], + }; + continue; + } + const mech = MECHANICAL.find((m) => m.re.test(key)); + if (mech) { + const newKey = mech.to(key.match(mech.re)); + map[key] = newKey; + meta[key] = { prose: mech.prose, mechanical: true, sites: recs.length }; + continue; + } + + const isAria = key.startsWith('aria/'); + const copy = isAria ? key.slice('aria/'.length) : key; + const r = recs[0]; + const namespacesUsed = [...new Set(recs.map((x) => namespaceOf(x.file)))]; + const leaf = camel(copy); + const suffix = suffixOf(r, isAria); + + // A key used from more than one namespace is shared copy; it belongs in `common.*` rather + // than being arbitrarily attributed to whichever component happens to be walked first. + const shared = namespacesUsed.length > 1; + const ns = shared ? 'common' : namespacesUsed[0]; + const comp = shared ? '' : componentSegment(r.file); + + // `message.messageStatus.…` repeats the namespace inside the component segment; strip it. + let compSeg = comp; + if (compSeg.toLowerCase().startsWith(ns.toLowerCase()) && compSeg.length > ns.length) { + const rest = compSeg.slice(ns.length); + compSeg = rest[0].toLowerCase() + rest.slice(1); + } + // …and drop it entirely when it just *is* the namespace. + const segs = + compSeg && compSeg.toLowerCase() !== ns.toLowerCase() + ? [ns, compSeg, leaf] + : [ns, leaf]; + let newKey = `${segs.join('.')}.${suffix}`; + // A leaf identical to its suffix ("label.label") adds nothing. + newKey = newKey.replace(new RegExp(`\\.${suffix}\\.${suffix}$`), `.${suffix}`); + + map[key] = newKey; + // Remember the parts so the leaf can be dropped later where it carries no information. + parts[key] = { ns, comp: compSeg, leaf, suffix }; + meta[key] = { + prose: true, + mechanical: false, + shared, + sites: recs.length, + plural: pluralBases.has(key), + interpolations: r.interpolations, + files: [...new Set(recs.map((x) => x.file))], + }; +} + +// en.json keys with no call site still need renaming (language/*, timestamp/*). +for (const k of Object.keys(en).map(bare)) { + if (map[k]) continue; + const mech = MECHANICAL.find((m) => m.re.test(k)); + if (mech) { + map[k] = mech.to(k.match(mech.re)); + meta[k] = { prose: mech.prose, mechanical: true, sites: 0 }; + } else { + review.push({ key: k, newKey: null, why: 'no call site and no mechanical rule' }); + } +} + +// For copy that is a whole sentence the leaf is a lossy re-encoding of the sentence and adds +// nothing ("poll.endPollAlert.wantEndPollNow.description"). Where `..` is +// already unique, drop the leaf and let the component + role name the key. +{ + const tripleCount = {}; + for (const [key, p] of Object.entries(parts)) { + if (!p.comp) continue; + const triple = `${p.ns}.${p.comp}.${p.suffix}`; + tripleCount[triple] = (tripleCount[triple] ?? 0) + 1; + parts[key].triple = triple; + } + for (const [key, p] of Object.entries(parts)) { + if (!p.triple || tripleCount[p.triple] !== 1) continue; + // Judge by the original copy, not the derived leaf: a short label ("Voice message + // deleted") still needs its leaf, a full sentence does not. + if (words(key.replace(/^aria\//, '')).length < 6) continue; + map[key] = p.triple; + } +} + +// Two old keys can land on the same new key because the leaf is derived from the copy with +// interpolation placeholders stripped ("Animated GIF" vs "Animated GIF: {{ title }}"). +// Disambiguate by folding the interpolation variables into the key, which is also the more +// descriptive name; fall back to a numeric suffix only if that is still not unique. +const groupByNewKey = () => { + const rev = new Map(); + for (const [oldK, newK] of Object.entries(map)) { + if (!rev.has(newK)) rev.set(newK, []); + rev.get(newK).push(oldK); + } + return rev; +}; + +for (const [, olds] of groupByNewKey()) { + if (olds.length < 2) continue; + for (const oldK of olds) { + const vars = (meta[oldK]?.interpolations ?? []).filter((v) => v !== 'count'); + if (!vars.length) continue; // the bare variant keeps the short key + const parts = map[oldK].split('.'); + const suffix = parts.pop(); + map[oldK] = [ + ...parts, + `with${vars.map((v) => v[0].toUpperCase() + v.slice(1)).join('And')}`, + suffix, + ].join('.'); + } +} + +for (const [, olds] of groupByNewKey()) { + if (olds.length < 2) continue; + olds.slice(1).forEach((oldK, i) => { + const parts = map[oldK].split('.'); + const suffix = parts.pop(); + map[oldK] = [...parts, String(i + 2), suffix].join('.'); + }); +} + +const collisions = [...groupByNewKey().entries()].filter(([, v]) => v.length > 1); + +// Emit sorted by new key so the file reads as a browsable table and diffs stay stable. +const entries = Object.entries(map).sort((a, b) => a[1].localeCompare(b[1])); +const keys = {}; +for (const [oldKey, newKey] of entries) { + const m = meta[oldKey] ?? {}; + keys[oldKey] = { + key: newKey, + // `prose: false` means the value is a formatter expression or plumbing, not English copy, + // so the codemod must NOT add an inline default for it. + prose: m.prose !== false, + ...(m.plural ? { plural: true } : {}), + ...(m.shared ? { shared: true } : {}), + }; +} + +fs.writeFileSync( + OUT, + JSON.stringify( + { + $comment: + 'Migration table: natural-language translation key -> namespaced key. Generated by ' + + 'scripts/i18n-migration/generate-key-map.mjs and reviewed by hand. Integrators who ' + + 'passed translationsForLanguage/registerTranslation dictionaries keyed on the old ' + + 'strings should use this to rename their keys.', + count: entries.length, + keys, + }, + null, + 2, + ) + '\n', +); +if (collisions.length || review.length) { + fs.writeFileSync( + OUT.replace(/\.json$/, '.report.json'), + JSON.stringify({ collisions, review }, null, 2) + '\n', + ); +} + +console.log('mapped keys: ', Object.keys(map).length); +console.log('collisions: ', collisions.length); +console.log('needs review: ', review.length); +console.log('longest new key: ', Math.max(...Object.values(map).map((k) => k.length))); +if (collisions.length) { + console.log('\n--- COLLISIONS ---'); + for (const [newK, olds] of collisions) { + console.log(` ${newK}`); + olds.forEach((o) => console.log(` <- ${JSON.stringify(o)}`)); + } +} +if (review.length) { + console.log('\n--- REVIEW ---'); + review.forEach((r) => + console.log( + ` ${JSON.stringify(r.key)} :: ${r.why}${r.namespaces ? ' ' + r.namespaces.join(',') : ''}`, + ), + ); +} diff --git a/scripts/i18n-migration/key-map.json b/scripts/i18n-migration/key-map.json new file mode 100644 index 0000000000..f2d4e64495 --- /dev/null +++ b/scripts/i18n-migration/key-map.json @@ -0,0 +1,2502 @@ +{ + "$comment": "Migration table: natural-language translation key -> namespaced key. Generated by scripts/i18n-migration/generate-key-map.mjs and reviewed by hand. Integrators who passed translationsForLanguage/registerTranslation dictionaries keyed on the old strings should use this to rename their keys.", + "count": 603, + "keys": { + "aria/Active": { + "key": "a11y.accessibleLabel.active.ariaLabel", + "prose": true + }, + "aria/{{ count }} unread message": { + "key": "a11y.accessibleLabel.unreadMessage.ariaLabel", + "prose": true, + "plural": true + }, + "New message from {{user}}": { + "key": "a11y.incomingMessageAnnouncements.newMessage.label", + "prose": true + }, + "aria/Command activated: {{ command }}": { + "key": "a11y.interactionAnnouncements.commandActivated.ariaLabel", + "prose": true + }, + "aria/Dropped \"{{ option }}\" at position {{ position }}.": { + "key": "a11y.interactionAnnouncements.droppedPosition.ariaLabel", + "prose": true + }, + "aria/Giphy canceled": { + "key": "a11y.interactionAnnouncements.giphyCanceled.ariaLabel", + "prose": true + }, + "aria/Giphy image changed": { + "key": "a11y.interactionAnnouncements.giphyImageChanged.ariaLabel", + "prose": true + }, + "aria/Giphy image changed: {{ title }}": { + "key": "a11y.interactionAnnouncements.giphyImageChanged.withTitle.ariaLabel", + "prose": true + }, + "aria/Giphy sent": { + "key": "a11y.interactionAnnouncements.giphySent.ariaLabel", + "prose": true + }, + "aria/No search results found": { + "key": "a11y.interactionAnnouncements.noSearchResultsFound.ariaLabel", + "prose": true + }, + "aria/Opened channel: {{ name }}": { + "key": "a11y.interactionAnnouncements.openedChannel.ariaLabel", + "prose": true + }, + "aria/Opened thread in {{ name }}": { + "key": "a11y.interactionAnnouncements.openedThread.ariaLabel", + "prose": true + }, + "aria/Picked up \"{{ option }}\". Use arrow keys to reorder. Press Space or Tab to drop.": { + "key": "a11y.interactionAnnouncements.pickedUpUseArrow.ariaLabel", + "prose": true + }, + "aria/Poll dialog opened": { + "key": "a11y.interactionAnnouncements.pollDialogOpened.ariaLabel", + "prose": true + }, + "aria/Poll sent": { + "key": "a11y.interactionAnnouncements.pollSent.ariaLabel", + "prose": true + }, + "aria/Press Enter to start typing": { + "key": "a11y.interactionAnnouncements.pressEnterStartTyping.ariaLabel", + "prose": true + }, + "aria/Recording paused": { + "key": "a11y.interactionAnnouncements.recordingPaused.ariaLabel", + "prose": true + }, + "aria/Recording resumed": { + "key": "a11y.interactionAnnouncements.recordingResumed.ariaLabel", + "prose": true + }, + "aria/Recording started": { + "key": "a11y.interactionAnnouncements.recordingStarted.ariaLabel", + "prose": true + }, + "aria/Removed option {{ option }}": { + "key": "a11y.interactionAnnouncements.removedOption.ariaLabel", + "prose": true + }, + "aria/Search cleared": { + "key": "a11y.interactionAnnouncements.searchCleared.ariaLabel", + "prose": true + }, + "aria/{{ count }} search results": { + "key": "a11y.interactionAnnouncements.searchResults.ariaLabel", + "prose": true, + "plural": true + }, + "aria/{{ count }} suggestions": { + "key": "a11y.interactionAnnouncements.suggestions.ariaLabel", + "prose": true, + "plural": true + }, + "aria/{{ count }} {{ suggestionsLabel }}": { + "key": "a11y.interactionAnnouncements.suggestionsWithLabel.ariaLabel", + "prose": true, + "plural": true + }, + "aria/User selected: {{ user }}": { + "key": "a11y.interactionAnnouncements.userSelected.ariaLabel", + "prose": true + }, + "aria/Voice message sent": { + "key": "a11y.interactionAnnouncements.voiceMessageSent.ariaLabel", + "prose": true + }, + "aria/Voice recording attached": { + "key": "a11y.interactionAnnouncements.voiceRecordingAttached.ariaLabel", + "prose": true + }, + "Generating...": { + "key": "aiState.indicator.generating.label", + "prose": true + }, + "Thinking...": { + "key": "aiState.indicator.thinking.label", + "prose": true + }, + "aria/Giphy actions": { + "key": "attachment.actions.giphyActions.ariaLabel", + "prose": true + }, + "aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.": { + "key": "attachment.actions.giphyPreviewOnlyVisible.ariaLabel", + "prose": true + }, + "Shuffle": { + "key": "attachment.actions.shuffle.label", + "prose": true + }, + "Live until {{ timestamp }}": { + "key": "attachment.geolocation.liveUntil.text", + "prose": true + }, + "Location sharing ended": { + "key": "attachment.geolocation.locationSharingEnded.text", + "prose": true + }, + "Open location in a map": { + "key": "attachment.geolocation.openLocationMap.ariaLabel", + "prose": true + }, + "Stop sharing": { + "key": "attachment.geolocation.stopSharing.text", + "prose": true + }, + "aria/Animated GIF": { + "key": "attachment.giphy.animatedGif.ariaLabel", + "prose": true + }, + "aria/Animated GIF: {{ title }}": { + "key": "attachment.giphy.animatedGif.withTitle.ariaLabel", + "prose": true + }, + "Open gallery at image {{ index }}": { + "key": "attachment.modalGallery.openGalleryImage.label", + "prose": true + }, + "Open image in gallery": { + "key": "attachment.modalGallery.openImageGallery.label", + "prose": true + }, + "this content could not be displayed": { + "key": "attachment.unableRenderCard.text", + "prose": true + }, + "Only visible to you": { + "key": "attachment.visibilityDisclaimer.onlyVisible.text", + "prose": true + }, + "Cannot seek in the recording": { + "key": "audioPlayback.audioPlayerNotifications.cannotSeekRecording.label", + "prose": true + }, + "Failed to play the recording": { + "key": "audioPlayback.audioPlayerNotifications.failedPlayRecording.label", + "prose": true + }, + "Recording format is not supported and cannot be reproduced": { + "key": "audioPlayback.audioPlayerNotifications.recordingFormatNotSupported.label", + "prose": true + }, + "aria/Seek audio position": { + "key": "audioPlayback.progressBar.seekAudioPosition.ariaLabel", + "prose": true + }, + "aria/Audio position {{ elapsed }} of {{ duration }}": { + "key": "audioPlayback.progressBarA11y.audioPosition.ariaLabel", + "prose": true + }, + "aria/Audio position {{ progress }} percent": { + "key": "audioPlayback.progressBarA11y.audioPositionPercent.ariaLabel", + "prose": true + }, + "aria/Image failed to load": { + "key": "baseImage.imagePlaceholder.imageFailedLoad.ariaLabel", + "prose": true + }, + "Channel Missing": { + "key": "channel.channelMissing.text", + "prose": true + }, + "aria/Channel details": { + "key": "channelDetail.avatarChannelDetail.channelDetails.ariaLabel", + "prose": true + }, + "aria/Open channel details": { + "key": "channelDetail.avatarChannelDetail.openChannelDetails.ariaLabel", + "prose": true + }, + "No files": { + "key": "channelDetail.channelFilesEmpty.noFiles.text", + "prose": true + }, + "Share a file to see it here": { + "key": "channelDetail.channelFilesEmpty.shareFileSee.text", + "prose": true + }, + "Files": { + "key": "channelDetail.channelFilesView.files.title", + "prose": true + }, + "Block user": { + "key": "channelDetail.channelManagementActions.blockUser.title", + "prose": true + }, + "Chat deleted": { + "key": "channelDetail.channelManagementActions.chatDeleted.text", + "prose": true + }, + "Delete chat": { + "key": "channelDetail.channelManagementActions.deleteChat.title", + "prose": true + }, + "Error blocking user": { + "key": "channelDetail.channelManagementActions.errorBlockingUser.text", + "prose": true + }, + "Error deleting chat": { + "key": "channelDetail.channelManagementActions.errorDeletingChat.text", + "prose": true + }, + "Error muting channel": { + "key": "channelDetail.channelManagementActions.errorMutingChannel.text", + "prose": true + }, + "Error muting user": { + "key": "channelDetail.channelManagementActions.errorMutingUser.text", + "prose": true + }, + "Error unblocking user": { + "key": "channelDetail.channelManagementActions.errorUnblockingUser.text", + "prose": true + }, + "Error unmuting channel": { + "key": "channelDetail.channelManagementActions.errorUnmutingChannel.text", + "prose": true + }, + "Error unmuting user": { + "key": "channelDetail.channelManagementActions.errorUnmutingUser.text", + "prose": true + }, + "Leave chat": { + "key": "channelDetail.channelManagementActions.leaveChat.title", + "prose": true + }, + "Mute chat": { + "key": "channelDetail.channelManagementActions.muteChat.title", + "prose": true + }, + "Mute user": { + "key": "channelDetail.channelManagementActions.muteUser.title", + "prose": true + }, + "This permanently deletes your message history with {{ user }}. This can't be undone.": { + "key": "channelDetail.channelManagementActions.permanentlyDeletesMessageHistory.description", + "prose": true + }, + "Are you sure you want to leave this channel?": { + "key": "channelDetail.channelManagementActions.sureWantLeaveChannel.description", + "prose": true + }, + "Unmute chat": { + "key": "channelDetail.channelManagementActions.unmuteChat.title", + "prose": true + }, + "Unmute user": { + "key": "channelDetail.channelManagementActions.unmuteUser.title", + "prose": true + }, + "This user will be able to message you again.": { + "key": "channelDetail.channelManagementActions.userAbleMessageAgain.description", + "prose": true + }, + "User muted": { + "key": "channelDetail.channelManagementActions.userMuted.text", + "prose": true + }, + "User unmuted": { + "key": "channelDetail.channelManagementActions.userUnmuted.text", + "prose": true + }, + "This user won't be able to message you anymore. You can unblock them anytime.": { + "key": "channelDetail.channelManagementActions.userWonTAble.description", + "prose": true + }, + "Changes saved": { + "key": "channelDetail.channelManagementView.changesSaved.text", + "prose": true + }, + "Contact info": { + "key": "channelDetail.channelManagementView.contactInfo.label", + "prose": true + }, + "Contact name": { + "key": "channelDetail.channelManagementView.contactName.label", + "prose": true + }, + "Edit": { + "key": "channelDetail.channelManagementView.edit.text", + "prose": true + }, + "Edit chat data": { + "key": "channelDetail.channelManagementView.editChatData.ariaLabel", + "prose": true + }, + "Edit contact": { + "key": "channelDetail.channelManagementView.editContact.label", + "prose": true + }, + "Edit group": { + "key": "channelDetail.channelManagementView.editGroup.label", + "prose": true + }, + "Failed to save changes": { + "key": "channelDetail.channelManagementView.failedSaveChanges.text", + "prose": true + }, + "Group info": { + "key": "channelDetail.channelManagementView.groupInfo.label", + "prose": true + }, + "Group name": { + "key": "channelDetail.channelManagementView.groupName.label", + "prose": true + }, + "Manage channel": { + "key": "channelDetail.channelManagementView.manageChannel.description", + "prose": true + }, + "Save": { + "key": "channelDetail.channelManagementView.save.text", + "prose": true + }, + "Upload Picture": { + "key": "channelDetail.channelManagementView.uploadPicture.text", + "prose": true + }, + "No photos or videos": { + "key": "channelDetail.channelMediaEmpty.noPhotosVideos.text", + "prose": true + }, + "Share a photo or video to see it here": { + "key": "channelDetail.channelMediaEmpty.sharePhotoVideoSee.text", + "prose": true + }, + "Next": { + "key": "channelDetail.channelMediaView.next.text", + "prose": true + }, + "aria/Next page": { + "key": "channelDetail.channelMediaView.nextPage.ariaLabel", + "prose": true + }, + "aria/Open image shared by {{ name }}": { + "key": "channelDetail.channelMediaView.openImageShared.ariaLabel", + "prose": true + }, + "aria/Open video shared by {{ name }}": { + "key": "channelDetail.channelMediaView.openVideoShared.ariaLabel", + "prose": true + }, + "Photos & videos": { + "key": "channelDetail.channelMediaView.photosVideos.title", + "prose": true + }, + "Previous": { + "key": "channelDetail.channelMediaView.previous.text", + "prose": true + }, + "aria/Previous page": { + "key": "channelDetail.channelMediaView.previousPage.ariaLabel", + "prose": true + }, + "{{ member }} will be able to message you again.": { + "key": "channelDetail.channelMemberActions.ableMessageAgain.description", + "prose": true + }, + "Error opening direct message": { + "key": "channelDetail.channelMemberActions.errorOpeningDirectMessage.text", + "prose": true + }, + "Error removing user": { + "key": "channelDetail.channelMemberActions.errorRemovingUser.text", + "prose": true + }, + "Remove {{ member }} from this channel?": { + "key": "channelDetail.channelMemberActions.removeChannel.description", + "prose": true + }, + "Remove user": { + "key": "channelDetail.channelMemberActions.removeUser.title", + "prose": true + }, + "Send direct message": { + "key": "channelDetail.channelMemberActions.sendDirectMessage.title", + "prose": true + }, + "Unblock user": { + "key": "channelDetail.channelMemberActions.unblockUser.title", + "prose": true + }, + "User removed": { + "key": "channelDetail.channelMemberActions.userRemoved.text", + "prose": true + }, + "{{ member }} won't be able to message you anymore.": { + "key": "channelDetail.channelMemberActions.wonTAbleMessage.description", + "prose": true + }, + "Last seen {{ timestamp }}": { + "key": "channelDetail.channelMemberDetail.lastSeen.label", + "prose": true + }, + "Member detail": { + "key": "channelDetail.channelMemberDetail.memberDetail.title", + "prose": true + }, + "Add {{ count }} members": { + "key": "channelDetail.channelMembersAdd.addMembers.text", + "prose": true, + "plural": true + }, + "Already a member": { + "key": "channelDetail.channelMembersAdd.alreadyMember.label", + "prose": true + }, + "Error adding members": { + "key": "channelDetail.channelMembersAdd.errorAddingMembers.text", + "prose": true + }, + "{{ count }} members added": { + "key": "channelDetail.channelMembersAdd.membersAdded.text", + "prose": true, + "plural": true + }, + "No user found": { + "key": "channelDetail.channelMembersAdd.noUserFound.text", + "prose": true + }, + "Admin": { + "key": "channelDetail.channelMembersBrowse.admin.label", + "prose": true + }, + "Moderator": { + "key": "channelDetail.channelMembersBrowse.moderator.label", + "prose": true + }, + "No member found": { + "key": "channelDetail.channelMembersBrowse.noMemberFound.text", + "prose": true + }, + "Owner": { + "key": "channelDetail.channelMembersBrowse.owner.label", + "prose": true + }, + "View member details for {{ member }}": { + "key": "channelDetail.channelMembersBrowse.viewMemberDetails.ariaLabel", + "prose": true + }, + "Actions": { + "key": "channelDetail.channelMembersHeader.actions.text", + "prose": true + }, + "Add": { + "key": "channelDetail.channelMembersHeader.add.text", + "prose": true + }, + "Add channel members": { + "key": "channelDetail.channelMembersHeader.addChannelMembers.ariaLabel", + "prose": true + }, + "Open members actions": { + "key": "channelDetail.channelMembersHeader.openMembersActions.ariaLabel", + "prose": true + }, + "Add members": { + "key": "channelDetail.channelMembersView.addMembers.label", + "prose": true + }, + "Browse channel members": { + "key": "channelDetail.channelMembersView.browseChannelMembers.description", + "prose": true + }, + "{{ count }} members": { + "key": "channelDetail.channelMembersView.members.title", + "prose": true, + "plural": true + }, + "No pinned messages": { + "key": "channelDetail.pinnedMessagesEmpty.noPinnedMessages.text", + "prose": true + }, + "Pin a message to see it here": { + "key": "channelDetail.pinnedMessagesEmpty.pinMessageSee.text", + "prose": true + }, + "Browse pinned messages": { + "key": "channelDetail.pinnedMessagesView.browsePinnedMessages.description", + "prose": true + }, + "No messages found": { + "key": "channelDetail.pinnedMessagesView.noMessagesFound.text", + "prose": true + }, + "Pinned message": { + "key": "channelDetail.pinnedMessagesView.pinnedMessage.label", + "prose": true + }, + "Pinned messages": { + "key": "channelDetail.pinnedMessagesView.pinnedMessages.title", + "prose": true + }, + "Open menu": { + "key": "channelDetail.sectionNavigatorHeader.openMenu.ariaLabel", + "prose": true + }, + "{{ memberCount }} members": { + "key": "channelHeader.online.members.label", + "prose": true + }, + "{{ watcherCount }} online": { + "key": "channelHeader.online.online.label", + "prose": true + }, + "aria/Channel list": { + "key": "channelList.channelList.ariaLabel", + "prose": true + }, + "Chats": { + "key": "channelList.header.chats.text", + "prose": true + }, + "Archive": { + "key": "channelListItem.archive.title", + "prose": true + }, + "aria/Attachment": { + "key": "channelListItem.attachment.ariaLabel", + "prose": true + }, + "🏙 Attachment...": { + "key": "channelListItem.attachment.text", + "prose": true + }, + "aria/Attachment {{ attachmentType }}": { + "key": "channelListItem.attachment.withAttachmentType.ariaLabel", + "prose": true + }, + "aria/{{ count }} attachment": { + "key": "channelListItem.attachmentCount.ariaLabel", + "prose": true, + "plural": true + }, + "aria/audio": { + "key": "channelListItem.audio.ariaLabel", + "prose": true + }, + "aria/Channel Actions": { + "key": "channelListItem.channelActions.ariaLabel", + "prose": true + }, + "Channel archived": { + "key": "channelListItem.channelArchived.text", + "prose": true + }, + "Direct message": { + "key": "channelListItem.channelDisplayName.directMessage.label", + "prose": true + }, + "Channel pinned": { + "key": "channelListItem.channelPinned.text", + "prose": true + }, + "Channel unarchived": { + "key": "channelListItem.channelUnarchived.text", + "prose": true + }, + "Channel unpinned": { + "key": "channelListItem.channelUnpinned.text", + "prose": true + }, + "📊 {{createdBy}} created: {{ pollName}}": { + "key": "channelListItem.created.text", + "prose": true + }, + "aria/Delivered": { + "key": "channelListItem.delivered.ariaLabel", + "prose": true + }, + "aria/Delivery status: {{ deliveryStatus }}": { + "key": "channelListItem.deliveryStatus.ariaLabel", + "prose": true + }, + "Failed to block user": { + "key": "channelListItem.failedBlockUser.text", + "prose": true + }, + "Failed to update channel archive status": { + "key": "channelListItem.failedUpdateChannelArchive.text", + "prose": true + }, + "Failed to update channel mute status": { + "key": "channelListItem.failedUpdateChannelMute.text", + "prose": true + }, + "Failed to update channel pinned status": { + "key": "channelListItem.failedUpdateChannelPinned.text", + "prose": true + }, + "aria/file": { + "key": "channelListItem.file.ariaLabel", + "prose": true + }, + "aria/GIF": { + "key": "channelListItem.gif.ariaLabel", + "prose": true + }, + "aria/image": { + "key": "channelListItem.image.ariaLabel", + "prose": true + }, + "aria/Last message: {{ messagePreview }}": { + "key": "channelListItem.lastMessage.withMessagePreview.ariaLabel", + "prose": true + }, + "aria/Last message from {{ sender }}: {{ messagePreview }}": { + "key": "channelListItem.lastMessage.withSenderAndMessagePreview.ariaLabel", + "prose": true + }, + "Leave Channel": { + "key": "channelListItem.leaveChannel.title", + "prose": true + }, + "aria/Message with attachments": { + "key": "channelListItem.messageAttachments.ariaLabel", + "prose": true + }, + "aria/There are no messages in this chat.": { + "key": "channelListItem.noMessagesChat.ariaLabel", + "prose": true + }, + "aria/Open Channel Actions Menu": { + "key": "channelListItem.openChannelActionsMenu.ariaLabel", + "prose": true + }, + "aria/Poll: {{ pollName }}": { + "key": "channelListItem.poll.ariaLabel", + "prose": true + }, + "aria/Read": { + "key": "channelListItem.read.ariaLabel", + "prose": true + }, + "aria/Sent": { + "key": "channelListItem.sent.ariaLabel", + "prose": true + }, + "aria/Shared a link": { + "key": "channelListItem.sharedLink.ariaLabel", + "prose": true + }, + "aria/Shared a link with title: {{ linkTitle }}": { + "key": "channelListItem.sharedLinkTitle.ariaLabel", + "prose": true + }, + "aria/Shared location": { + "key": "channelListItem.sharedLocation.ariaLabel", + "prose": true + }, + "📍Shared location": { + "key": "channelListItem.sharedLocation.text", + "prose": true + }, + "Unarchive": { + "key": "channelListItem.unarchive.title", + "prose": true + }, + "Unblock User": { + "key": "channelListItem.unblockUser.title", + "prose": true + }, + "aria/video": { + "key": "channelListItem.video.ariaLabel", + "prose": true + }, + "aria/voice message": { + "key": "channelListItem.voiceMessage.ariaLabel", + "prose": true + }, + "📊 {{votedBy}} voted: {{pollOptionText}}": { + "key": "channelListItem.voted.text", + "prose": true + }, + "Waiting for network…": { + "key": "chat.reportLostConnection.waitingNetwork.text", + "prose": true + }, + "ban-command-args": { + "key": "command.ban.args", + "prose": true + }, + "ban-command-description": { + "key": "command.ban.description", + "prose": true + }, + "giphy-command-args": { + "key": "command.giphy.args", + "prose": true + }, + "giphy-command-description": { + "key": "command.giphy.description", + "prose": true + }, + "mute-command-args": { + "key": "command.mute.args", + "prose": true + }, + "mute-command-description": { + "key": "command.mute.description", + "prose": true + }, + "unban-command-args": { + "key": "command.unban.args", + "prose": true + }, + "unban-command-description": { + "key": "command.unban.description", + "prose": true + }, + "unmute-command-args": { + "key": "command.unmute.args", + "prose": true + }, + "unmute-command-description": { + "key": "command.unmute.description", + "prose": true + }, + "Add reaction": { + "key": "common.addReaction.text", + "prose": true, + "shared": true + }, + "Anonymous": { + "key": "common.anonymous.label", + "prose": true, + "shared": true + }, + "Back": { + "key": "common.back.label", + "prose": true, + "shared": true + }, + "Block User": { + "key": "common.blockUser.title", + "prose": true, + "shared": true + }, + "Cancel": { + "key": "common.cancel.label", + "prose": true, + "shared": true + }, + "Channel muted": { + "key": "common.channelMuted.text", + "prose": true, + "shared": true + }, + "Channel unmuted": { + "key": "common.channelUnmuted.text", + "prose": true, + "shared": true + }, + "Close": { + "key": "common.close.ariaLabel", + "prose": true, + "shared": true + }, + "Create a question, add options, and configure poll settings": { + "key": "common.createQuestionAddOptions.label", + "prose": true, + "shared": true + }, + "Current location": { + "key": "common.currentLocation.text", + "prose": true, + "shared": true + }, + "Delete": { + "key": "common.delete.text", + "prose": true, + "shared": true + }, + "aria/Download attachment": { + "key": "common.downloadAttachment.ariaLabel", + "prose": true, + "shared": true + }, + "Download Attachment": { + "key": "common.downloadAttachment.title", + "prose": true, + "shared": true + }, + "Edit Message": { + "key": "common.editMessage.text", + "prose": true, + "shared": true + }, + "Empty message...": { + "key": "common.emptyMessage.text", + "prose": true, + "shared": true + }, + "Error deleting message": { + "key": "common.errorDeletingMessage.label", + "prose": true, + "shared": true + }, + "Error muting a user ...": { + "key": "common.errorMutingUser.label", + "prose": true, + "shared": true + }, + "Error pinning message": { + "key": "common.errorPinningMessage.label", + "prose": true, + "shared": true + }, + "Error removing message pin": { + "key": "common.errorRemovingMessagePin.label", + "prose": true, + "shared": true + }, + "Error unmuting a user ...": { + "key": "common.errorUnmutingUser.label", + "prose": true, + "shared": true + }, + "Failed to leave channel": { + "key": "common.failedLeaveChannel.text", + "prose": true, + "shared": true + }, + "aria/Last activity: {{ time }}": { + "key": "common.lastActivity.ariaLabel", + "prose": true, + "shared": true + }, + "Left channel": { + "key": "common.leftChannel.text", + "prose": true, + "shared": true + }, + "Live location": { + "key": "common.liveLocation.text", + "prose": true, + "shared": true + }, + "Location": { + "key": "common.location.text", + "prose": true, + "shared": true + }, + "Message deleted": { + "key": "common.messageDeleted.text", + "prose": true, + "shared": true + }, + "Message pinned": { + "key": "common.messagePinned.label", + "prose": true, + "shared": true + }, + "Mute": { + "key": "common.mute.title", + "prose": true, + "shared": true + }, + "{{ user }} has been muted": { + "key": "common.muted.label", + "prose": true, + "shared": true + }, + "{{count}} new messages": { + "key": "common.newMessages.label", + "prose": true, + "plural": true, + "shared": true + }, + "Nothing yet...": { + "key": "common.nothingYet.text", + "prose": true, + "shared": true + }, + "Offline": { + "key": "common.offline.label", + "prose": true, + "shared": true + }, + "Online": { + "key": "common.online.label", + "prose": true, + "shared": true + }, + "aria/Open Reaction Selector": { + "key": "common.openReactionSelector.ariaLabel", + "prose": true, + "shared": true + }, + "aria/Pause": { + "key": "common.pause.ariaLabel", + "prose": true, + "shared": true + }, + "Pin": { + "key": "common.pin.title", + "prose": true, + "shared": true + }, + "aria/Play": { + "key": "common.play.ariaLabel", + "prose": true, + "shared": true + }, + "Playback speed {{ rate }}x": { + "key": "common.playbackSpeedX.label", + "prose": true, + "shared": true + }, + "Poll": { + "key": "common.poll.label", + "prose": true, + "shared": true + }, + "Reminder set": { + "key": "common.reminderSet.text", + "prose": true, + "shared": true + }, + "replyCount": { + "key": "common.replyCount.label", + "prose": true, + "plural": true, + "shared": true + }, + "All results loaded": { + "key": "common.resultsLoaded.label", + "prose": true, + "shared": true + }, + "aria/Retry upload": { + "key": "common.retryUpload.ariaLabel", + "prose": true, + "shared": true + }, + "Saved for later": { + "key": "common.savedLater.text", + "prose": true, + "shared": true + }, + "Search": { + "key": "common.search.ariaLabel", + "prose": true, + "shared": true + }, + "Send": { + "key": "common.send.label", + "prose": true, + "shared": true + }, + "Threads": { + "key": "common.threads.text", + "prose": true, + "shared": true + }, + "Unblock": { + "key": "common.unblock.ariaLabel", + "prose": true, + "shared": true + }, + "Unmute": { + "key": "common.unmute.title", + "prose": true, + "shared": true + }, + "{{ user }} has been unmuted": { + "key": "common.unmuted.label", + "prose": true, + "shared": true + }, + "Unpin": { + "key": "common.unpin.title", + "prose": true, + "shared": true + }, + "Unsupported attachment": { + "key": "common.unsupportedAttachment.text", + "prose": true, + "shared": true + }, + "User blocked": { + "key": "common.userBlocked.text", + "prose": true, + "shared": true + }, + "User unblocked": { + "key": "common.userUnblocked.text", + "prose": true, + "shared": true + }, + "User uploaded content": { + "key": "common.userUploadedContent.label", + "prose": true, + "shared": true + }, + "Voice message": { + "key": "common.voiceMessage.label", + "prose": true, + "shared": true + }, + "You": { + "key": "common.you.label", + "prose": true, + "shared": true + }, + "aria/Close callout dialog": { + "key": "dialog.callout.closeCalloutDialog.ariaLabel", + "prose": true + }, + "aria/Back to parent menu button": { + "key": "dialog.contextMenu.backParentMenuButton.ariaLabel", + "prose": true + }, + "aria/Submenu": { + "key": "dialog.contextMenu.submenu.ariaLabel", + "prose": true + }, + "Go back": { + "key": "dialog.prompt.goBack.ariaLabel", + "prose": true + }, + "Close dialog": { + "key": "dialog.viewer.closeDialog.ariaLabel", + "prose": true + }, + "duration/Message reminder": { + "key": "duration.messageReminder", + "prose": false + }, + "duration/Remind Me": { + "key": "duration.remindMe", + "prose": false + }, + "duration/Share Location": { + "key": "duration.shareLocation", + "prose": false + }, + "aria/Emoji picker": { + "key": "emojiPicker.emojiPicker.ariaLabel", + "prose": true + }, + "No conversations yet": { + "key": "emptyState.indicator.noConversationsYet.label", + "prose": true + }, + "No items exist": { + "key": "emptyState.indicator.noItemsExist.text", + "prose": true + }, + "Send a message to start the conversation": { + "key": "emptyState.indicator.sendMessageStartConversation.label", + "prose": true + }, + "aria/File upload": { + "key": "fileUpload.uploadButton.fileUpload.ariaLabel", + "prose": true + }, + "aria/Decrease value": { + "key": "form.numericInput.decreaseValue.ariaLabel", + "prose": true + }, + "aria/Increase value": { + "key": "form.numericInput.increaseValue.ariaLabel", + "prose": true + }, + "aria/{{ setting }} disabled": { + "key": "form.switchField.disabled.ariaLabel", + "prose": true + }, + "aria/{{ setting }} enabled": { + "key": "form.switchField.enabled.ariaLabel", + "prose": true + }, + "Next image": { + "key": "gallery.ui.nextImage.ariaLabel", + "prose": true + }, + "Previous image": { + "key": "gallery.ui.previousImage.ariaLabel", + "prose": true + }, + "language/af": { + "key": "language.af", + "prose": true + }, + "language/am": { + "key": "language.am", + "prose": true + }, + "language/ar": { + "key": "language.ar", + "prose": true + }, + "language/az": { + "key": "language.az", + "prose": true + }, + "language/bg": { + "key": "language.bg", + "prose": true + }, + "language/bn": { + "key": "language.bn", + "prose": true + }, + "language/bs": { + "key": "language.bs", + "prose": true + }, + "language/cs": { + "key": "language.cs", + "prose": true + }, + "language/da": { + "key": "language.da", + "prose": true + }, + "language/de": { + "key": "language.de", + "prose": true + }, + "language/el": { + "key": "language.el", + "prose": true + }, + "language/en": { + "key": "language.en", + "prose": true + }, + "language/es": { + "key": "language.es", + "prose": true + }, + "language/es-MX": { + "key": "language.es-MX", + "prose": true + }, + "language/et": { + "key": "language.et", + "prose": true + }, + "language/fa": { + "key": "language.fa", + "prose": true + }, + "language/fa-AF": { + "key": "language.fa-AF", + "prose": true + }, + "language/fi": { + "key": "language.fi", + "prose": true + }, + "language/fr": { + "key": "language.fr", + "prose": true + }, + "language/fr-CA": { + "key": "language.fr-CA", + "prose": true + }, + "language/ha": { + "key": "language.ha", + "prose": true + }, + "language/he": { + "key": "language.he", + "prose": true + }, + "language/hi": { + "key": "language.hi", + "prose": true + }, + "language/hr": { + "key": "language.hr", + "prose": true + }, + "language/ht": { + "key": "language.ht", + "prose": true + }, + "language/hu": { + "key": "language.hu", + "prose": true + }, + "language/id": { + "key": "language.id", + "prose": true + }, + "language/it": { + "key": "language.it", + "prose": true + }, + "language/ja": { + "key": "language.ja", + "prose": true + }, + "language/ka": { + "key": "language.ka", + "prose": true + }, + "language/ko": { + "key": "language.ko", + "prose": true + }, + "language/lt": { + "key": "language.lt", + "prose": true + }, + "language/lv": { + "key": "language.lv", + "prose": true + }, + "language/ms": { + "key": "language.ms", + "prose": true + }, + "language/nl": { + "key": "language.nl", + "prose": true + }, + "language/no": { + "key": "language.no", + "prose": true + }, + "language/pl": { + "key": "language.pl", + "prose": true + }, + "language/ps": { + "key": "language.ps", + "prose": true + }, + "language/pt": { + "key": "language.pt", + "prose": true + }, + "language/ro": { + "key": "language.ro", + "prose": true + }, + "language/ru": { + "key": "language.ru", + "prose": true + }, + "language/sk": { + "key": "language.sk", + "prose": true + }, + "language/sl": { + "key": "language.sl", + "prose": true + }, + "language/so": { + "key": "language.so", + "prose": true + }, + "language/sq": { + "key": "language.sq", + "prose": true + }, + "language/sr": { + "key": "language.sr", + "prose": true + }, + "language/sv": { + "key": "language.sv", + "prose": true + }, + "language/sw": { + "key": "language.sw", + "prose": true + }, + "language/ta": { + "key": "language.ta", + "prose": true + }, + "language/th": { + "key": "language.th", + "prose": true + }, + "language/tl": { + "key": "language.tl", + "prose": true + }, + "language/tr": { + "key": "language.tr", + "prose": true + }, + "language/uk": { + "key": "language.uk", + "prose": true + }, + "language/ur": { + "key": "language.ur", + "prose": true + }, + "language/vi": { + "key": "language.vi", + "prose": true + }, + "language/zh": { + "key": "language.zh", + "prose": true + }, + "language/zh-TW": { + "key": "language.zh-TW", + "prose": true + }, + "Error: {{ errorMessage }}": { + "key": "loading.errorIndicator.error.text", + "prose": true + }, + "aria/Percent complete": { + "key": "loading.progressIndicators.percentComplete.ariaLabel", + "prose": true + }, + "Load more": { + "key": "loadMore.button.loadMore.label", + "prose": true + }, + "Attach": { + "key": "location.shareLocationDialog.attach.text", + "prose": true + }, + "Select your current location and optionally enable live location sharing": { + "key": "location.shareLocationDialog.description", + "prose": true + }, + "Share": { + "key": "location.shareLocationDialog.share.text", + "prose": true + }, + "Share live location for": { + "key": "location.shareLocationDialog.shareLiveLocation.title", + "prose": true + }, + "Share Location": { + "key": "location.shareLocationDialog.shareLocation.title", + "prose": true + }, + "aria/Cancel recording": { + "key": "mediaRecorder.audioRecorderRecording.cancelRecording.ariaLabel", + "prose": true + }, + "aria/Complete recording": { + "key": "mediaRecorder.audioRecorderRecording.completeRecording.ariaLabel", + "prose": true + }, + "aria/Pause recording": { + "key": "mediaRecorder.audioRecorderRecording.pauseRecording.ariaLabel", + "prose": true + }, + "aria/Resume recording": { + "key": "mediaRecorder.audioRecorderRecording.resumeRecording.ariaLabel", + "prose": true + }, + "Voice message deleted": { + "key": "mediaRecorder.audioRecorderRecording.voiceMessageDeleted.text", + "prose": true + }, + "aria/Start recording audio": { + "key": "mediaRecorder.audioRecordingButton.startRecordingAudio.ariaLabel", + "prose": true + }, + "An error has occurred during the recording processing": { + "key": "mediaRecorder.error.processing", + "prose": true + }, + "An error has occurred during recording": { + "key": "mediaRecorder.error.recording", + "prose": true + }, + "Error starting recording": { + "key": "mediaRecorder.error.start", + "prose": true + }, + "To start recording, allow the camera access in your browser": { + "key": "mediaRecorder.permissionDenied.camera.body", + "prose": true + }, + "Allow access to camera": { + "key": "mediaRecorder.permissionDenied.camera.heading", + "prose": true + }, + "To start recording, allow the microphone access in your browser": { + "key": "mediaRecorder.permissionDenied.microphone.body", + "prose": true + }, + "Allow access to microphone": { + "key": "mediaRecorder.permissionDenied.microphone.heading", + "prose": true + }, + "mention/Channel Description": { + "key": "mention.channel.description", + "prose": true + }, + "mention/Here Description": { + "key": "mention.here.description", + "prose": true + }, + "Also sent in channel": { + "key": "message.alsoSent.alsoSentChannel.text", + "prose": true + }, + "Replied to a thread": { + "key": "message.alsoSent.repliedThread.text", + "prose": true + }, + "View": { + "key": "message.alsoSent.view.text", + "prose": true + }, + "{{ commaSeparatedUsers }}, and {{ lastUser }}": { + "key": "message.and.withCommaSeparatedUsersAndLastUser.label", + "prose": true + }, + "{{ firstUser }} and {{ secondUser }}": { + "key": "message.and.withFirstUserAndSecondUser.label", + "prose": true + }, + "Message was blocked by moderation policies": { + "key": "message.blocked.text", + "prose": true + }, + "Edited": { + "key": "message.editedIndicator.edited.text", + "prose": true + }, + "{{ commaSeparatedUsers }} and {{ moreCount }} more": { + "key": "message.more.label", + "prose": true + }, + "Pinned by You": { + "key": "message.pinIndicator.pinned.label", + "prose": true + }, + "Pinned by {{ name }}": { + "key": "message.pinIndicator.pinned.withName.label", + "prose": true + }, + "Due {{ timeLeft }}": { + "key": "message.reminderNotification.due.label", + "prose": true + }, + "Due since {{ dueSince }}": { + "key": "message.reminderNotification.dueSince.label", + "prose": true + }, + "Delivered": { + "key": "message.status.delivered.text", + "prose": true + }, + "Sending...": { + "key": "message.status.sending.text", + "prose": true + }, + "Sent": { + "key": "message.status.sent.text", + "prose": true + }, + "aria/Message,": { + "key": "message.text.message.ariaLabel", + "prose": true + }, + "aria/Message from {{ user }},": { + "key": "message.text.message.withUser.ariaLabel", + "prose": true + }, + "Original": { + "key": "message.translationIndicator.original.text", + "prose": true + }, + "Translated": { + "key": "message.translationIndicator.translated.text", + "prose": true + }, + "Translated from {{ language }}": { + "key": "message.translationIndicator.translated.withLanguage.text", + "prose": true + }, + "View original": { + "key": "message.translationIndicator.viewOriginal.text", + "prose": true + }, + "View translation": { + "key": "message.translationIndicator.viewTranslation.text", + "prose": true + }, + "aria/Review bounced message": { + "key": "message.ui.reviewBouncedMessage.ariaLabel", + "prose": true + }, + "aria/Block User": { + "key": "messageActions.blockUser.ariaLabel", + "prose": true + }, + "aria/Bookmark Message": { + "key": "messageActions.bookmarkMessage.ariaLabel", + "prose": true + }, + "Copy Message": { + "key": "messageActions.copyMessage.text", + "prose": true + }, + "aria/Copy Message Text": { + "key": "messageActions.copyMessageText.ariaLabel", + "prose": true + }, + "aria/Delete Message": { + "key": "messageActions.deleteMessage.ariaLabel", + "prose": true + }, + "Delete message": { + "key": "messageActions.deleteMessageAlert.deleteMessage.title", + "prose": true + }, + "Are you sure you want to delete this message?": { + "key": "messageActions.deleteMessageAlert.description", + "prose": true + }, + "Download {{ fileName }}": { + "key": "messageActions.downloadSubmenu.download.label", + "prose": true + }, + "Download All": { + "key": "messageActions.downloadSubmenu.download.text", + "prose": true + }, + "Download attachment {{ number }}": { + "key": "messageActions.downloadSubmenu.downloadAttachment.label", + "prose": true + }, + "aria/Edit Message": { + "key": "messageActions.editMessage.ariaLabel", + "prose": true + }, + "Error adding flag": { + "key": "messageActions.errorAddingFlag.text", + "prose": true + }, + "Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.": { + "key": "messageActions.errorMarkingMessageUnread.text", + "prose": true + }, + "Flag": { + "key": "messageActions.flag.text", + "prose": true + }, + "aria/Flag Message": { + "key": "messageActions.flagMessage.ariaLabel", + "prose": true + }, + "aria/Mark Message Unread": { + "key": "messageActions.markMessageUnread.ariaLabel", + "prose": true + }, + "Mark as unread": { + "key": "messageActions.markUnread.text", + "prose": true + }, + "aria/Message Actions": { + "key": "messageActions.messageActions.ariaLabel", + "prose": true + }, + "Message marked as unread": { + "key": "messageActions.messageMarkedUnread.text", + "prose": true + }, + "Message has been successfully flagged": { + "key": "messageActions.messageSuccessfullyFlagged.text", + "prose": true + }, + "Message unpinned": { + "key": "messageActions.messageUnpinned.text", + "prose": true + }, + "aria/Mute User": { + "key": "messageActions.muteUser.ariaLabel", + "prose": true + }, + "aria/Open Message Actions Menu": { + "key": "messageActions.openMessageActionsMenu.ariaLabel", + "prose": true + }, + "aria/Open Thread": { + "key": "messageActions.openThread.ariaLabel", + "prose": true + }, + "aria/Pin Message": { + "key": "messageActions.pinMessage.ariaLabel", + "prose": true + }, + "aria/Quote Message": { + "key": "messageActions.quoteMessage.ariaLabel", + "prose": true + }, + "Quote Reply": { + "key": "messageActions.quoteReply.text", + "prose": true + }, + "Remind me": { + "key": "messageActions.remindMe.text", + "prose": true + }, + "aria/Remind Me Message": { + "key": "messageActions.remindMeMessage.ariaLabel", + "prose": true + }, + "Remind Me": { + "key": "messageActions.remindMeSubmenu.remindMe.text", + "prose": true + }, + "aria/Remove Reminder": { + "key": "messageActions.removeReminder.ariaLabel", + "prose": true + }, + "Remove reminder": { + "key": "messageActions.removeReminder.text", + "prose": true + }, + "aria/Remove Save For Later": { + "key": "messageActions.removeSaveLater.ariaLabel", + "prose": true + }, + "Remove save for later": { + "key": "messageActions.removeSaveLater.text", + "prose": true + }, + "Resend": { + "key": "messageActions.resend.text", + "prose": true + }, + "aria/Resend Message": { + "key": "messageActions.resendMessage.ariaLabel", + "prose": true + }, + "Save for later": { + "key": "messageActions.saveLater.text", + "prose": true + }, + "Thread Reply": { + "key": "messageActions.threadReply.text", + "prose": true + }, + "aria/Unmute User": { + "key": "messageActions.unmuteUser.ariaLabel", + "prose": true + }, + "aria/Unpin Message": { + "key": "messageActions.unpinMessage.ariaLabel", + "prose": true + }, + "Review this message and choose whether to delete it, edit it, or send it anyway": { + "key": "messageBounce.prompt.description", + "prose": true + }, + "Send Anyway": { + "key": "messageBounce.prompt.sendAnyway.text", + "prose": true + }, + "This message did not meet our content guidelines": { + "key": "messageBounce.prompt.title", + "prose": true + }, + "aria/Show preview": { + "key": "messageComposer.attachmentPreviewRoot.showPreview.ariaLabel", + "prose": true + }, + "aria/Attachment Actions": { + "key": "messageComposer.attachmentSelector.attachmentActions.ariaLabel", + "prose": true + }, + "Commands": { + "key": "messageComposer.attachmentSelector.commands.text", + "prose": true + }, + "File": { + "key": "messageComposer.attachmentSelector.file.text", + "prose": true + }, + "aria/Open Attachment Selector": { + "key": "messageComposer.attachmentSelector.openAttachmentSelector.ariaLabel", + "prose": true + }, + "File too large": { + "key": "messageComposer.audioAttachmentPreview.fileTooLarge.text", + "prose": true + }, + "Retry upload": { + "key": "messageComposer.audioAttachmentPreview.retryUpload.text", + "prose": true + }, + "Upload blocked": { + "key": "messageComposer.audioAttachmentPreview.uploadBlocked.text", + "prose": true + }, + "Upload error": { + "key": "messageComposer.audioAttachmentPreview.uploadError.text", + "prose": true + }, + "Upload failed": { + "key": "messageComposer.audioAttachmentPreview.uploadFailed.text", + "prose": true + }, + "Exit command {{ command }}": { + "key": "messageComposer.commandChip.exitCommand.ariaLabel", + "prose": true + }, + "aria/Back to attachments": { + "key": "messageComposer.commandsMenu.backAttachments.ariaLabel", + "prose": true + }, + "Instant commands": { + "key": "messageComposer.commandsMenu.instantCommands.text", + "prose": true + }, + "Drag your files here": { + "key": "messageComposer.dragDropUpload.dragFiles.text", + "prose": true + }, + "Some of the files will not be accepted": { + "key": "messageComposer.dragDropUpload.someFilesNotAccepted.text", + "prose": true + }, + "Live for {{duration}}": { + "key": "messageComposer.geolocationPreview.live.text", + "prose": true + }, + "Location: {{ coordinates }}": { + "key": "messageComposer.geolocationPreview.location.text", + "prose": true + }, + "aria/Remove location attachment": { + "key": "messageComposer.geolocationPreview.removeLocationAttachment.ariaLabel", + "prose": true + }, + "Shared location": { + "key": "messageComposer.geolocationPreview.sharedLocation.title", + "prose": true + }, + "Attach files": { + "key": "messageComposer.icons.attachFiles.text", + "prose": true + }, + "aria/Cancel Reply": { + "key": "messageComposer.quotedMessagePreview.cancelReply.ariaLabel", + "prose": true + }, + "{{ count }} files": { + "key": "messageComposer.quotedMessagePreview.files.label", + "prose": true, + "plural": true + }, + "aria/Jump to quoted message": { + "key": "messageComposer.quotedMessagePreview.jumpQuotedMessage.ariaLabel", + "prose": true + }, + "Photo": { + "key": "messageComposer.quotedMessagePreview.photo.label", + "prose": true + }, + "{{ count }} photos": { + "key": "messageComposer.quotedMessagePreview.photos.label", + "prose": true, + "plural": true + }, + "Reply": { + "key": "messageComposer.quotedMessagePreview.reply.text", + "prose": true + }, + "Reply to {{ authorName }}": { + "key": "messageComposer.quotedMessagePreview.reply.withAuthorName.text", + "prose": true + }, + "Video": { + "key": "messageComposer.quotedMessagePreview.video.label", + "prose": true + }, + "{{ count }} videos": { + "key": "messageComposer.quotedMessagePreview.videos.label", + "prose": true, + "plural": true + }, + "Voice message {{ duration }}": { + "key": "messageComposer.quotedMessagePreview.voiceMessage.label", + "prose": true + }, + "aria/Remove attachment": { + "key": "messageComposer.removeAttachmentPreview.removeAttachment.ariaLabel", + "prose": true + }, + "aria/Send": { + "key": "messageComposer.sendButton.send.ariaLabel", + "prose": true + }, + "Also send in channel": { + "key": "messageComposer.sendChannelCheckbox.alsoSendChannel.label", + "prose": true + }, + "Also send as a direct message": { + "key": "messageComposer.sendChannelCheckbox.alsoSendDirectMessage.label", + "prose": true + }, + "Send message request failed": { + "key": "messageComposer.sendMessageFn.sendMessageRequestFailed.text", + "prose": true + }, + "aria/Stop AI Generation": { + "key": "messageComposer.stopAiGeneration.stopAiGeneration.ariaLabel", + "prose": true + }, + "Edit message request failed": { + "key": "messageComposer.updateMessageFn.editMessageRequestFailed.text", + "prose": true + }, + "New Messages!": { + "key": "messageList.newMessageNotification.newMessages.label", + "prose": true + }, + "aria/Jump to latest message": { + "key": "messageList.scrollLatestMessage.jumpLatestMessage.ariaLabel", + "prose": true + }, + "aria/Mark messages as read": { + "key": "messageList.unreadMessagesNotification.markMessagesRead.ariaLabel", + "prose": true + }, + "{{count}} unread": { + "key": "messageList.unreadMessagesNotification.unread.text", + "prose": true, + "plural": true + }, + "Unread messages": { + "key": "messageList.unreadMessagesNotification.unreadMessages.text", + "prose": true + }, + "fileCount": { + "key": "messagePreview.latestMessagePreview.fileCount.label", + "prose": true, + "plural": true + }, + "imageCount": { + "key": "messagePreview.latestMessagePreview.imageCount.label", + "prose": true, + "plural": true + }, + "linkCount": { + "key": "messagePreview.latestMessagePreview.linkCount.label", + "prose": true, + "plural": true + }, + "Message failed to send": { + "key": "messagePreview.latestMessagePreview.messageFailedSend.text", + "prose": true + }, + "videoCount": { + "key": "messagePreview.latestMessagePreview.videoCount.label", + "prose": true, + "plural": true + }, + "voiceMessageCount": { + "key": "messagePreview.latestMessagePreview.voiceMessageCount.label", + "prose": true, + "plural": true + }, + "File is required for upload attachment": { + "key": "notification.attachmentFileMissing", + "prose": true + }, + "Local upload attachment missing local id": { + "key": "notification.attachmentIdMissing", + "prose": true + }, + "Attachment upload blocked due to {{reason}}": { + "key": "notification.attachmentUploadBlockedWithReason", + "prose": true + }, + "Error uploading attachment": { + "key": "notification.attachmentUploadFailed", + "prose": true + }, + "Attachment upload failed due to {{reason}}": { + "key": "notification.attachmentUploadFailedWithReason", + "prose": true + }, + "Wait until all attachments have uploaded": { + "key": "notification.attachmentUploadInProgress", + "prose": true + }, + "Error reproducing the recording": { + "key": "notification.audioPlaybackError", + "prose": true + }, + "Command not available": { + "key": "notification.commandDisabled", + "prose": true + }, + "Command not available while editing": { + "key": "notification.commandDisabledWhileEditing", + "prose": true + }, + "Command not available while replying": { + "key": "notification.commandDisabledWhileReplying", + "prose": true + }, + "aria/Dismiss notification": { + "key": "notification.dismissNotification.ariaLabel", + "prose": true + }, + "Failed to jump to the first unread message": { + "key": "notification.jumpToFirstUnreadFailed", + "prose": true + }, + "aria/Notifications": { + "key": "notification.list.notifications.ariaLabel", + "prose": true + }, + "Failed to retrieve location": { + "key": "notification.locationGetFailed", + "prose": true + }, + "Failed to share location": { + "key": "notification.locationShareFailed", + "prose": true + }, + "Failed to create the poll": { + "key": "notification.pollCreateFailed", + "prose": true + }, + "Failed to create the poll due to {{reason}}": { + "key": "notification.pollCreateFailedWithReason", + "prose": true + }, + "Failed to end the poll": { + "key": "notification.pollEndFailed", + "prose": true + }, + "Failed to end the poll due to {{reason}}": { + "key": "notification.pollEndFailedWithReason", + "prose": true + }, + "Poll ended": { + "key": "notification.pollEndSuccess", + "prose": true + }, + "Reached the vote limit. Remove an existing vote first.": { + "key": "notification.pollVoteLimit", + "prose": true + }, + "size limit": { + "key": "notification.reason.sizeLimit", + "prose": true + }, + "unknown error": { + "key": "notification.reason.unknownError", + "prose": true + }, + "unsupported file type": { + "key": "notification.reason.unsupportedFileType", + "prose": true + }, + "Thread has not been found": { + "key": "notification.replySearchFailed", + "prose": true + }, + "Suggest an option": { + "key": "poll.actions.suggestOption.label", + "prose": true + }, + "View {{count}} comments": { + "key": "poll.actions.viewComments.label", + "prose": true, + "plural": true + }, + "View results": { + "key": "poll.actions.viewResults.label", + "prose": true + }, + "Add a comment": { + "key": "poll.addCommentPrompt.addComment.label", + "prose": true + }, + "Add a comment to your poll answer": { + "key": "poll.addCommentPrompt.addCommentPollAnswer.label", + "prose": true + }, + "This field cannot be empty or contain only spaces": { + "key": "poll.addCommentPrompt.fieldCannotEmptyContain.label", + "prose": true + }, + "Update": { + "key": "poll.addCommentPrompt.update.text", + "prose": true + }, + "Update your comment": { + "key": "poll.addCommentPrompt.updateComment.label", + "prose": true + }, + "Update the comment attached to your poll answer": { + "key": "poll.addCommentPrompt.updateCommentAttachedPoll.label", + "prose": true + }, + "Review comments submitted with poll answers": { + "key": "poll.answerList.description", + "prose": true + }, + "Poll comments": { + "key": "poll.answerList.pollComments.title", + "prose": true + }, + "Allow others to add comments": { + "key": "poll.creationDialog.allowOthersAddComments.description", + "prose": true + }, + "Anonymous poll": { + "key": "poll.creationDialog.anonymousPoll.title", + "prose": true + }, + "Create poll": { + "key": "poll.creationDialog.createPoll.title", + "prose": true + }, + "Hide who voted": { + "key": "poll.creationDialog.hideWhoVoted.description", + "prose": true + }, + "Let others add options": { + "key": "poll.creationDialog.letOthersAddOptions.description", + "prose": true + }, + "Poll sent": { + "key": "poll.creationDialog.pollSent.text", + "prose": true + }, + "Send poll": { + "key": "poll.creationDialog.sendPoll.text", + "prose": true + }, + "Do you want to end this poll now? Nobody will be able to vote in this poll anymore.": { + "key": "poll.endPollAlert.description", + "prose": true + }, + "End poll": { + "key": "poll.endPollAlert.endPoll.text", + "prose": true + }, + "End this poll?": { + "key": "poll.endPollAlert.endPoll.title", + "prose": true + }, + "Select one": { + "key": "poll.header.selectOne.label", + "prose": true + }, + "Select one or more": { + "key": "poll.header.selectOneMore.label", + "prose": true + }, + "Select up to {{count}}": { + "key": "poll.header.selectUp.label", + "prose": true, + "plural": true + }, + "Vote ended": { + "key": "poll.header.voteEnded.label", + "prose": true + }, + "Choose between 2 to 10 options": { + "key": "poll.multipleAnswersField.chooseBetween210.description", + "prose": true + }, + "Enforce unique vote is enabled": { + "key": "poll.multipleAnswersField.enforceUniqueVoteEnabled.label", + "prose": true + }, + "Limit votes per person": { + "key": "poll.multipleAnswersField.limitVotesPerPerson.title", + "prose": true + }, + "Maximum votes per person": { + "key": "poll.multipleAnswersField.maximumVotesPerPerson.ariaLabel", + "prose": true + }, + "Multiple votes": { + "key": "poll.multipleAnswersField.multipleVotes.title", + "prose": true + }, + "Only numbers are allowed": { + "key": "poll.multipleAnswersField.onlyNumbersAllowed.label", + "prose": true + }, + "Select more than one option": { + "key": "poll.multipleAnswersField.selectMoreThanOne.description", + "prose": true + }, + "Type a number from 2 to 10": { + "key": "poll.multipleAnswersField.typeNumber210.label", + "prose": true + }, + "Ask a question": { + "key": "poll.nameField.askQuestion.placeholder", + "prose": true + }, + "Error": { + "key": "poll.nameField.error.text", + "prose": true + }, + "Question is required": { + "key": "poll.nameField.questionRequired.label", + "prose": true + }, + "Add an option": { + "key": "poll.optionFieldSet.addOption.placeholder", + "prose": true + }, + "aria/Option {{ position }}": { + "key": "poll.optionFieldSet.option.ariaLabel", + "prose": true + }, + "aria/This option can be reordered and removed.": { + "key": "poll.optionFieldSet.optionCanReorderedRemoved.ariaLabel", + "prose": true + }, + "Option is empty": { + "key": "poll.optionFieldSet.optionEmpty.label", + "prose": true + }, + "Options": { + "key": "poll.optionFieldSet.options.label", + "prose": true + }, + "aria/Options can now be reordered and removed.": { + "key": "poll.optionFieldSet.optionsCanNowReordered.ariaLabel", + "prose": true + }, + "aria/Remove option: {{ option }}": { + "key": "poll.optionFieldSet.removeOption.ariaLabel", + "prose": true + }, + "+{{count}} more options": { + "key": "poll.optionList.moreOptions.label", + "prose": true, + "plural": true + }, + "aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.": { + "key": "poll.optionReorder.pressSpaceSelectOption.ariaLabel", + "prose": true + }, + "aria/Reorder option {{ position }}": { + "key": "poll.optionReorder.reorderOption.ariaLabel", + "prose": true + }, + "aria/Reorder \"{{ option }}\" at position {{ position }} of {{ total }}": { + "key": "poll.optionReorder.reorderPosition.ariaLabel", + "prose": true + }, + "Review all options available in this poll": { + "key": "poll.optionsFull.description", + "prose": true + }, + "Poll options": { + "key": "poll.optionsFull.pollOptions.title", + "prose": true + }, + "Question {{ optionOrderNumber}}": { + "key": "poll.optionVotes.question.text", + "prose": true + }, + "View all": { + "key": "poll.optionVotes.view.text", + "prose": true + }, + "{{count}} votes": { + "key": "poll.optionVotes.votes.text", + "prose": true, + "plural": true + }, + "placeholder/PollComment": { + "key": "poll.pollComment.placeholder", + "prose": true + }, + "placeholder/PollOptionSuggestion": { + "key": "poll.pollOptionSuggestion.placeholder", + "prose": true + }, + "Question": { + "key": "poll.question.question.text", + "prose": true + }, + "Poll results": { + "key": "poll.results.pollResults.title", + "prose": true + }, + "Review poll results and open an option to see detailed votes": { + "key": "poll.results.reviewPollResultsOpen.description", + "prose": true + }, + "Review who voted for this option": { + "key": "poll.results.reviewWhoVotedOption.description", + "prose": true + }, + "totalVoteCount": { + "key": "poll.results.totalVoteCount.text", + "prose": true, + "plural": true + }, + "Votes": { + "key": "poll.results.votes.title", + "prose": true + }, + "Suggest a new option to add to this poll": { + "key": "poll.suggestPollOption.description", + "prose": true + }, + "Option already exists": { + "key": "poll.suggestPollOption.optionAlreadyExists.label", + "prose": true + }, + "Error fetching reactions": { + "key": "reactions.fetchReactions.errorFetchingReactions.text", + "prose": true + }, + "aria/Reaction list": { + "key": "reactions.messageReactions.reactionList.ariaLabel", + "prose": true + }, + "aria/Select Reaction: {{ reactionName }}": { + "key": "reactions.messageReactions.selectReaction.ariaLabel", + "prose": true + }, + "{{ count }} reactions": { + "key": "reactions.messageReactionsDetail.reactions.text", + "prose": true, + "plural": true + }, + "Tap to remove: {{ reactionName }}": { + "key": "reactions.messageReactionsDetail.tapRemove.ariaLabel", + "prose": true + }, + "Tap to remove": { + "key": "reactions.messageReactionsDetail.tapRemove.text", + "prose": true + }, + "aria/Clear search": { + "key": "search.bar.clearSearch.ariaLabel", + "prose": true + }, + "aria/Exit search": { + "key": "search.bar.exitSearch.ariaLabel", + "prose": true + }, + "aria/Select User Channel: {{ name }}": { + "key": "search.resultItem.selectUserChannel.ariaLabel", + "prose": true + }, + "aria/Search results": { + "key": "search.results.searchResults.ariaLabel", + "prose": true + }, + "aria/Search results header filter button for: {{ source }}": { + "key": "search.resultsHeader.ariaLabel", + "prose": true + }, + "search-results-header-filter-source-button-label--channels": { + "key": "search.resultsHeader.filterSource.channels", + "prose": true + }, + "search-results-header-filter-source-button-label--messages": { + "key": "search.resultsHeader.filterSource.messages", + "prose": true + }, + "search-results-header-filter-source-button-label--users": { + "key": "search.resultsHeader.filterSource.users", + "prose": true + }, + "Start typing to search": { + "key": "search.resultsPresearch.startTypingSearch.text", + "prose": true + }, + "No results found": { + "key": "search.sourceResults.noResultsFound.text", + "prose": true + }, + "Searching for {{ searchSourceType }}...": { + "key": "search.sourceResults.searching.text", + "prose": true + }, + "Channels": { + "key": "slotLayout.chatView.channels.text", + "prose": true + }, + "aria/Chat view controls": { + "key": "slotLayout.chatView.chatViewControls.ariaLabel", + "prose": true + }, + "aria/Open channels view": { + "key": "slotLayout.chatView.openChannelsView.ariaLabel", + "prose": true + }, + "aria/Open threads view": { + "key": "slotLayout.chatView.openThreadsView.ariaLabel", + "prose": true + }, + "aria/Open threads view with unread threads": { + "key": "slotLayout.chatView.openThreadsViewUnread.ariaLabel", + "prose": true, + "plural": true + }, + "aria/Message input": { + "key": "textareaComposer.messageInput.ariaLabel", + "prose": true + }, + "Notify all {{ role }} members": { + "key": "textareaComposer.roleItem.notifyMembers.label", + "prose": true + }, + "aria/Command Suggestions": { + "key": "textareaComposer.suggestionList.commandSuggestions.ariaLabel", + "prose": true + }, + "aria/Emoji Suggestions": { + "key": "textareaComposer.suggestionList.emojiSuggestions.ariaLabel", + "prose": true + }, + "aria/Mention Suggestions": { + "key": "textareaComposer.suggestionList.mentionSuggestions.ariaLabel", + "prose": true + }, + "aria/Suggestions": { + "key": "textareaComposer.suggestionList.suggestions.ariaLabel", + "prose": true + }, + "Search GIFs": { + "key": "textareaComposer.textareaPlaceholder.searchGiFs.label", + "prose": true + }, + "Send a message": { + "key": "textareaComposer.textareaPlaceholder.sendMessage.label", + "prose": true + }, + "Slow mode, wait {{ seconds }}s...": { + "key": "textareaComposer.textareaPlaceholder.slowModeWaitS.label", + "prose": true + }, + "aria/Close thread": { + "key": "thread.header.closeThread.ariaLabel", + "prose": true + }, + "Thread": { + "key": "thread.header.thread.text", + "prose": true + }, + "aria/Chat: {{ channelName }}": { + "key": "threadList.chat.ariaLabel", + "prose": true + }, + "Reply to a message to start a thread": { + "key": "threadList.empty.text", + "prose": true + }, + "aria/Thread: {{ messagePreview }}": { + "key": "threadList.thread.ariaLabel", + "prose": true + }, + "aria/Thread list": { + "key": "threadList.threadList.ariaLabel", + "prose": true + }, + "ThreadListUnseenThreadsBanner/loading": { + "key": "threadList.unseenBanner.loading", + "prose": true + }, + "ThreadListUnseenThreadsBanner/unreadThreads": { + "key": "threadList.unseenBanner.unreadThreads", + "prose": true + }, + "timestamp/ChannelDetailPinnedMessageTimestamp": { + "key": "timestamp.ChannelDetailPinnedMessageTimestamp", + "prose": false + }, + "timestamp/ChannelMembersLastActive": { + "key": "timestamp.ChannelMembersLastActive", + "prose": false + }, + "timestamp/ChannelPreviewTimestamp": { + "key": "timestamp.ChannelPreviewTimestamp", + "prose": false + }, + "timestamp/DateSeparator": { + "key": "timestamp.DateSeparator", + "prose": false + }, + "timestamp/LiveLocation": { + "key": "timestamp.LiveLocation", + "prose": false + }, + "timestamp/MessageTimestamp": { + "key": "timestamp.MessageTimestamp", + "prose": false + }, + "timestamp/PollVote": { + "key": "timestamp.PollVote", + "prose": false + }, + "timestamp/PollVoteTooltip": { + "key": "timestamp.PollVoteTooltip", + "prose": false + }, + "timestamp/relativeDaysAgo": { + "key": "timestamp.relativeDaysAgo", + "prose": true + }, + "timestamp/relativeToday": { + "key": "timestamp.relativeToday", + "prose": true + }, + "timestamp/relativeWeeksAgo": { + "key": "timestamp.relativeWeeksAgo", + "prose": true + }, + "timestamp/relativeYesterday": { + "key": "timestamp.relativeYesterday", + "prose": true + }, + "timestamp/ReminderNotification": { + "key": "timestamp.ReminderNotification", + "prose": false + }, + "timestamp/SystemMessage": { + "key": "timestamp.SystemMessage", + "prose": false + }, + "translationBuilderTopic/notification": { + "key": "translationBuilderTopic.notification", + "prose": false + }, + "{{ count }} people are typing": { + "key": "typing.manyUsers", + "prose": true, + "plural": true + }, + "{{ typing }} is typing": { + "key": "typing.singleUser", + "prose": true + }, + "{{ typing }} are typing": { + "key": "typing.twoUsers", + "prose": true + }, + "Play video": { + "key": "videoPlayer.videoThumbnail.playVideo.ariaLabel", + "prose": true + } + } +} diff --git a/scripts/i18n-migration/normalize-test-t.mjs b/scripts/i18n-migration/normalize-test-t.mjs new file mode 100644 index 0000000000..238a00c019 --- /dev/null +++ b/scripts/i18n-migration/normalize-test-t.mjs @@ -0,0 +1,162 @@ +// Normalises every `t` stub in the test suite onto one implementation that mirrors i18next: +// positional `defaultValue`, `defaultValue_one`/`_other` plurals, and `{{ variable }}` +// interpolation. Stubs inside `vi.mock` / `vi.hoisted` get an inlined copy, since a top-level +// import is still in its TDZ when those run. +import ts from 'typescript'; +import fs from 'node:fs'; +import path from 'node:path'; + +// Files whose `t` stub is the subject of the assertion (call-argument spies, deliberate +// undefined-returning spies, or extra key-specific behaviour) and must not be rewritten. +const SKIP = new Set([ + 'src/i18n/__tests__/NotificationTranslationBuilder.test.ts', + 'src/i18n/__tests__/utils.test.ts', + 'src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx', + 'src/components/Attachment/__tests__/Audio.test.tsx', +]); + +const INLINE = `(( + key: string, + second?: unknown, + third?: unknown, + ) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = (options.count === 1 + ? options.defaultValue_one + : options.defaultValue_other) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + })`; + +const files = []; +(function walk(dir) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (/\.test\.tsx?$/.test(e.name)) files.push(p); + } +})('src'); + +const relativeImport = (from) => { + const rel = path + .relative(path.dirname(from), 'src/mock-builders/translator') + .replace(/\\/g, '/'); + return rel.startsWith('.') ? rel : `./${rel}`; +}; + +let changed = 0; +let inlined = 0; +let shared = 0; + +for (const file of files) { + if (SKIP.has(file)) continue; + const text = fs.readFileSync(file, 'utf8'); + const sf = ts.createSourceFile( + file, + text, + ts.ScriptTarget.Latest, + true, + file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS, + ); + + const hoistedRanges = []; + (function collect(node) { + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === 'vi' && + (node.expression.name.text === 'mock' || node.expression.name.text === 'hoisted') + ) { + hoistedRanges.push([node.getStart(sf), node.getEnd()]); + } + ts.forEachChild(node, collect); + })(sf); + const isHoisted = (pos) => hoistedRanges.some(([s, e]) => pos >= s && pos < e); + + // Names that hold a translator stub in these tests. + const NAMES = new Set([ + 't', + 'tAria', + 'tMock', + 'translate', + 'mockTranslation', + 'translator', + ]); + const edits = []; + (function visit(node) { + let target = null; + if ( + (ts.isPropertyAssignment(node) || ts.isVariableDeclaration(node)) && + ts.isIdentifier(node.name) && + NAMES.has(node.name.text) + ) { + target = ts.isPropertyAssignment(node) ? node.initializer : node.initializer; + } + if (target) { + let inner = target; + while (ts.isAsExpression(inner) || ts.isParenthesizedExpression(inner)) + inner = inner.expression; + let viWrapped = false; + if ( + ts.isCallExpression(inner) && + ts.isPropertyAccessExpression(inner.expression) && + ts.isIdentifier(inner.expression.expression) && + inner.expression.expression.text === 'vi' && + inner.expression.name.text === 'fn' && + inner.arguments.length === 1 + ) { + viWrapped = true; + inner = inner.arguments[0]; + while (ts.isAsExpression(inner) || ts.isParenthesizedExpression(inner)) + inner = inner.expression; + } + // Only rewrite function-shaped stubs; leave spies and references alone. + if (ts.isArrowFunction(inner) || ts.isFunctionExpression(inner)) { + const hoisted = isHoisted(inner.getStart(sf)); + if (hoisted) inlined++; + else shared++; + edits.push({ + start: inner.getStart(sf), + end: inner.getEnd(), + text: hoisted ? INLINE : 'mockT', + needsImport: !hoisted, + viWrapped, + }); + } + } + ts.forEachChild(node, visit); + })(sf); + + if (!edits.length) continue; + edits.sort((a, b) => b.start - a.start); + let out = text; + for (const e of edits) out = out.slice(0, e.start) + e.text + out.slice(e.end); + + if ( + edits.some((e) => e.needsImport) && + !/from\s+['"][^'"]*mock-builders\/translator['"]/.test(out) + ) { + const stmt = `import { mockT } from '${relativeImport(file)}';`; + const lastImport = [...out.matchAll(/^import .*?;$/gms)].pop(); + out = lastImport + ? `${out.slice(0, lastImport.index + lastImport[0].length)}\n${stmt}${out.slice(lastImport.index + lastImport[0].length)}` + : `${stmt}\n${out}`; + } + + fs.writeFileSync(file, out); + changed++; +} + +console.log('files updated:', changed, '| shared mockT:', shared, '| inlined:', inlined); diff --git a/src/a11y/accessibleLabel.ts b/src/a11y/accessibleLabel.ts index b548b1d0ef..6f58c79c31 100644 --- a/src/a11y/accessibleLabel.ts +++ b/src/a11y/accessibleLabel.ts @@ -51,12 +51,16 @@ type WithT = { t: TranslationContextValue['t'] }; export const activeLabelPart: AccessibleLabelPart = ({ active, t, -}) => (active ? t('aria/Active') : undefined); +}) => (active ? t('a11y.accessibleLabel.active.ariaLabel', 'Active') : undefined); /** Announces the unread count when there is one. */ export const unreadCountLabelPart: AccessibleLabelPart< WithT & { unreadCount?: number } > = ({ t, unreadCount }) => typeof unreadCount === 'number' && unreadCount > 0 - ? t('aria/{{ count }} unread message', { count: unreadCount }) + ? t('a11y.accessibleLabel.unreadMessage.ariaLabel', { + count: unreadCount, + defaultValue_one: '{{ count }} unread message', + defaultValue_other: '{{ count }} unread messages', + }) : undefined; diff --git a/src/components/AIStateIndicator/AIStateIndicator.tsx b/src/components/AIStateIndicator/AIStateIndicator.tsx index 5405a847a0..7962e4a69c 100644 --- a/src/components/AIStateIndicator/AIStateIndicator.tsx +++ b/src/components/AIStateIndicator/AIStateIndicator.tsx @@ -17,8 +17,8 @@ export const AIStateIndicator = ({ const channel = channelFromProps || channelFromContext; const { aiState } = useAIState(channel); const allowedStates = { - [AIStates.Thinking]: t('Thinking...'), - [AIStates.Generating]: t('Generating...'), + [AIStates.Thinking]: t('aiState.indicator.thinking.label', 'Thinking...'), + [AIStates.Generating]: t('aiState.indicator.generating.label', 'Generating...'), }; return aiState in allowedStates ? ( diff --git a/src/components/Accessibility/NotificationAnnouncer.tsx b/src/components/Accessibility/NotificationAnnouncer.tsx index c0d00fed84..7d91d1c523 100644 --- a/src/components/Accessibility/NotificationAnnouncer.tsx +++ b/src/components/Accessibility/NotificationAnnouncer.tsx @@ -100,7 +100,7 @@ export const NotificationAnnouncer = ({ seenNotificationIdsRef.current.add(notification.id); if (!notificationFilter(notification)) return; - const message = t('translationBuilderTopic/notification', { + const message = t('translationBuilderTopic.notification', { notification, value: notification.message, }); diff --git a/src/components/Accessibility/__tests__/NotificationAnnouncer.test.tsx b/src/components/Accessibility/__tests__/NotificationAnnouncer.test.tsx index bb0e3bc6b9..257fa7bd2c 100644 --- a/src/components/Accessibility/__tests__/NotificationAnnouncer.test.tsx +++ b/src/components/Accessibility/__tests__/NotificationAnnouncer.test.tsx @@ -13,6 +13,7 @@ import { TranslationProvider } from '../../../context'; import { mockTranslationContextValue } from 'mock-builders'; import type { Notification } from '../../../../../stream-chat-js/src'; +import { mockT } from '../../../mock-builders/translator'; vi.mock('../../Notifications/hooks/useNotifications', () => ({ useNotifications: vi.fn(), @@ -28,16 +29,7 @@ const notificationBase: Notification = { severity: 'info', }; -const mockTranslation = (key: string, options?: Record) => { - if ( - key === 'translationBuilderTopic/notification' && - typeof options?.value === 'string' - ) { - return options.value; - } - - return key; -}; +const mockTranslation = mockT; type RenderNotificationAnnouncerProps = { buildNotificationAnnouncement?: NotificationAnnouncementBuilder; diff --git a/src/components/Accessibility/hooks/__tests__/useIncomingMessageAnnouncements.test.tsx b/src/components/Accessibility/hooks/__tests__/useIncomingMessageAnnouncements.test.tsx index 4c1184bb0b..93f7023718 100644 --- a/src/components/Accessibility/hooks/__tests__/useIncomingMessageAnnouncements.test.tsx +++ b/src/components/Accessibility/hooks/__tests__/useIncomingMessageAnnouncements.test.tsx @@ -7,16 +7,24 @@ import type { Channel, Event, LocalMessage } from 'stream-chat'; const { announceMock, tMock } = vi.hoisted(() => ({ announceMock: vi.fn(), - tMock: vi.fn((key: string, options?: { count?: number; user?: string }) => { - if (key === '{{count}} new messages') { - return `${options?.count} new messages`; + tMock: vi.fn((key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; } - - if (key === 'New message from {{user}}') { - return `New message from ${options?.user}`; - } - - return key; + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); }), })); diff --git a/src/components/Accessibility/hooks/__tests__/useInteractionAnnouncements.test.tsx b/src/components/Accessibility/hooks/__tests__/useInteractionAnnouncements.test.tsx index 8eba94f70d..1592643edc 100644 --- a/src/components/Accessibility/hooks/__tests__/useInteractionAnnouncements.test.tsx +++ b/src/components/Accessibility/hooks/__tests__/useInteractionAnnouncements.test.tsx @@ -2,19 +2,30 @@ import { renderHook } from '@testing-library/react'; import { useInteractionAnnouncements } from '../useInteractionAnnouncements'; +// Mirrors i18next: render the inline English `defaultValue` (positional, or the +// `defaultValue_one`/`_other` plural forms) and interpolate `{{ variable }}` from the options. +// Inlined rather than imported from mock-builders because `vi.hoisted` runs before imports. const { announceMock, tMock } = vi.hoisted(() => ({ announceMock: vi.fn(), - tMock: vi.fn((key: string, params?: Record) => - Object.keys(params ?? {}).reduce( - (acc, paramKey) => - acc.replace( - new RegExp(`\\{\\{\\s${paramKey}\\s\\}\\}`, 'g'), - String(params?.[paramKey]), - ), - // strip the `aria/` prefix to mimic the natural-language key fallback - key.replace(/^aria\//, ''), - ), - ), + tMock: vi.fn((key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }), })); vi.mock('../../useAriaLiveAnnouncer', () => ({ diff --git a/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts b/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts index b031353e46..0f2d3375bd 100644 --- a/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts +++ b/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts @@ -32,7 +32,10 @@ const isAnnounceableIncomingMessage = ( const getSenderName = ( message: MessageResponse, t: ReturnType['t'], -) => message.user?.name?.trim() || message.user?.id || t('Anonymous'); +) => + message.user?.name?.trim() || + message.user?.id || + t('common.anonymous.label', 'Anonymous'); export type UseIncomingMessageAnnouncementsParams = { activeThreadId?: string; @@ -67,12 +70,24 @@ export const useIncomingMessageAnnouncements = ({ if (pendingAnnouncementBatch.count === 1) { announce( - t('New message from {{user}}', { - user: pendingAnnouncementBatch.firstSender || t('Anonymous'), - }), + t( + 'a11y.incomingMessageAnnouncements.newMessage.label', + 'New message from {{user}}', + { + user: + pendingAnnouncementBatch.firstSender || + t('common.anonymous.label', 'Anonymous'), + }, + ), ); } else { - announce(t('{{count}} new messages', { count: pendingAnnouncementBatch.count })); + announce( + t('common.newMessages.label', { + count: pendingAnnouncementBatch.count, + defaultValue_one: '{{count}} new message', + defaultValue_other: '{{count}} new messages', + }), + ); } pendingAnnouncementBatch.count = 0; diff --git a/src/components/Accessibility/hooks/useInteractionAnnouncements.ts b/src/components/Accessibility/hooks/useInteractionAnnouncements.ts index fdbe0c4ce5..c19165ded9 100644 --- a/src/components/Accessibility/hooks/useInteractionAnnouncements.ts +++ b/src/components/Accessibility/hooks/useInteractionAnnouncements.ts @@ -93,88 +93,148 @@ const INTERACTION_MESSAGES: { // Confirms the new audio playback speed after the user cycles it with the playback-rate button. // Reuses the existing (already-localized) button label so the spoken text matches the control. 'audioPlayer.playbackRateChanged': (t, params) => - t('Playback speed {{ rate }}x', { rate: params.rate }), + t('common.playbackSpeedX.label', 'Playback speed {{ rate }}x', { rate: params.rate }), // Confirms which channel was opened after selecting it from the list. Delayed (see // INTERACTION_DELAY_MS) so it lands AFTER the screen reader's announcement of the newly-focused // element (the message composer auto-focuses on channel change) rather than competing with — and // being superseded by — that native focus read-out. A provider-managed delay (not a per-row // debounce) so it still fires when selecting the channel unmounts the list (mobile). 'channel.opened': (t, params) => - t('aria/Opened channel: {{ name }}', { name: params.name }), + t( + 'a11y.interactionAnnouncements.openedChannel.ariaLabel', + 'Opened channel: {{ name }}', + { name: params.name }, + ), // Confirms which slash command was activated after picking it from the Instant Commands menu. // Delayed (INTERACTION_DELAY_MS) for the same reason as the "opened" confirmations: selecting a // command closes the menu and focuses the composer textarea (whose name changes, e.g. to // "Search GIFs"), so the screen reader reads that focus first — this lands after it. A provider // delay (not a debounce) because the menu that fired it unmounts on selection. 'command.selected': (t, params) => - t('aria/Command activated: {{ command }}', { command: params.command }), - 'giphy.canceled': (t) => t('aria/Giphy canceled'), - 'giphy.sent': (t) => t('aria/Giphy sent'), + t( + 'a11y.interactionAnnouncements.commandActivated.ariaLabel', + 'Command activated: {{ command }}', + { command: params.command }, + ), + 'giphy.canceled': (t) => + t('a11y.interactionAnnouncements.giphyCanceled.ariaLabel', 'Giphy canceled'), + 'giphy.sent': (t) => + t('a11y.interactionAnnouncements.giphySent.ariaLabel', 'Giphy sent'), // Giphy payloads rarely carry a human title, so include it only when present; otherwise a // generic "changed" confirmation. Both literal keys are extracted by i18next-cli. 'giphy.shuffled': (t, params) => params.title - ? t('aria/Giphy image changed: {{ title }}', { title: params.title }) - : t('aria/Giphy image changed'), + ? t( + 'a11y.interactionAnnouncements.giphyImageChanged.withTitle.ariaLabel', + 'Giphy image changed: {{ title }}', + { title: params.title }, + ) + : t( + 'a11y.interactionAnnouncements.giphyImageChanged.ariaLabel', + 'Giphy image changed', + ), // Spoken on poll-dialog open. Reuses the already-localized visible description for the middle // clause (so the spoken and visible text stay consistent) and adds two short aria-only phrases: // an explicit "opened" confirmation and the Enter affordance to step into the Question field. 'poll.dialogOpened': (t) => - `${t('aria/Poll dialog opened')}. ${t( + `${t('a11y.interactionAnnouncements.pollDialogOpened.ariaLabel', 'Poll dialog opened')}. ${t( + 'common.createQuestionAddOptions.label', 'Create a question, add options, and configure poll settings', - )}. ${t('aria/Press Enter to start typing')}.`, + )}. ${t('a11y.interactionAnnouncements.pressEnterStartTyping.ariaLabel', 'Press Enter to start typing')}.`, // Keyboard reorder pickup/drop. Assertive (see INTERACTION_PRIORITIES) — immediate drag feedback // that must not be queued behind other polite messages. 'poll.optionDropped': (t, params) => - t('aria/Dropped "{{ option }}" at position {{ position }}.', { - option: params.option, - position: params.position, - }), + t( + 'a11y.interactionAnnouncements.droppedPosition.ariaLabel', + 'Dropped "{{ option }}" at position {{ position }}.', + { + option: params.option, + position: params.position, + }, + ), 'poll.optionPickedUp': (t, params) => t( - 'aria/Picked up "{{ option }}". Use arrow keys to reorder. Press Space or Tab to drop.', + 'a11y.interactionAnnouncements.pickedUpUseArrow.ariaLabel', + 'Picked up "{{ option }}". Use arrow keys to reorder. Press Space or Tab to drop.', { option: params.option }, ), // Confirms a poll option was removed, naming it by its text (or positional fallback). Polite so it // queues behind the focus move to the next option field instead of interrupting it. 'poll.optionRemoved': (t, params) => - t('aria/Removed option {{ option }}', { option: params.option }), - 'poll.sent': (t) => t('aria/Poll sent'), - 'search.cleared': (t) => t('aria/Search cleared'), + t( + 'a11y.interactionAnnouncements.removedOption.ariaLabel', + 'Removed option {{ option }}', + { option: params.option }, + ), + 'poll.sent': (t) => t('a11y.interactionAnnouncements.pollSent.ariaLabel', 'Poll sent'), + 'search.cleared': (t) => + t('a11y.interactionAnnouncements.searchCleared.ariaLabel', 'Search cleared'), // The number of items currently listed in the search results; an empty result set is spelled out // rather than announced as "0". When the list is fully loaded, the end-of-list status is folded // into THIS one announcement (reusing the visible footer's "All results loaded" text) instead of a // competing second live-region message that would supersede the count. 'search.resultCount': (t, params) => { - if (params.count <= 0) return t('aria/No search results found'); - const count = t('aria/{{ count }} search results', { count: params.count }); - return params.allResultsLoaded ? `${count}. ${t('All results loaded')}` : count; + if (params.count <= 0) + return t( + 'a11y.interactionAnnouncements.noSearchResultsFound.ariaLabel', + 'No search results found', + ); + const count = t('a11y.interactionAnnouncements.searchResults.ariaLabel', { + count: params.count, + defaultValue_one: '{{ count }} search result', + defaultValue_other: '{{ count }} search results', + }); + return params.allResultsLoaded + ? `${count}. ${t('common.resultsLoaded.label', 'All results loaded')}` + : count; }, // Name the suggestion type by reusing the already-localized list label ("5 Command // Suggestions") so the user knows what the results are; fall back to the bare count when no // label is given. Both literal keys are extracted. 'suggestions.count': (t, params) => params.suggestionsLabel - ? t('aria/{{ count }} {{ suggestionsLabel }}', { + ? t('a11y.interactionAnnouncements.suggestionsWithLabel.ariaLabel', { count: params.count, + defaultValue_one: '{{ count }} {{ suggestionsLabel }}', + defaultValue_other: '{{ count }} {{ suggestionsLabel }}', suggestionsLabel: params.suggestionsLabel, }) - : t('aria/{{ count }} suggestions', { count: params.count }), + : t('a11y.interactionAnnouncements.suggestions.ariaLabel', { + count: params.count, + defaultValue_one: '{{ count }} suggestion', + defaultValue_other: '{{ count }} suggestions', + }), // Confirms which thread was opened after selecting it from the list; `name` is the thread's // channel display title. Delayed for the same reason as `channel.opened` (the thread composer // auto-focuses on open, and its focus announcement would otherwise supersede this one). 'thread.opened': (t, params) => - t('aria/Opened thread in {{ name }}', { name: params.name }), + t( + 'a11y.interactionAnnouncements.openedThread.ariaLabel', + 'Opened thread in {{ name }}', + { name: params.name }, + ), 'user.selected': (t, params) => - t('aria/User selected: {{ user }}', { user: params.user }), + t( + 'a11y.interactionAnnouncements.userSelected.ariaLabel', + 'User selected: {{ user }}', + { user: params.user }, + ), // Voice recorder lifecycle — discrete, immediate, polite confirmations. Cancellation is NOT here: // it already emits an app notification ("Voice message deleted") announced by NotificationAnnouncer, // and recording errors surface as error notifications; adding them here would double-announce. - 'voiceRecording.attached': (t) => t('aria/Voice recording attached'), - 'voiceRecording.paused': (t) => t('aria/Recording paused'), - 'voiceRecording.resumed': (t) => t('aria/Recording resumed'), - 'voiceRecording.sent': (t) => t('aria/Voice message sent'), - 'voiceRecording.started': (t) => t('aria/Recording started'), + 'voiceRecording.attached': (t) => + t( + 'a11y.interactionAnnouncements.voiceRecordingAttached.ariaLabel', + 'Voice recording attached', + ), + 'voiceRecording.paused': (t) => + t('a11y.interactionAnnouncements.recordingPaused.ariaLabel', 'Recording paused'), + 'voiceRecording.resumed': (t) => + t('a11y.interactionAnnouncements.recordingResumed.ariaLabel', 'Recording resumed'), + 'voiceRecording.sent': (t) => + t('a11y.interactionAnnouncements.voiceMessageSent.ariaLabel', 'Voice message sent'), + 'voiceRecording.started': (t) => + t('a11y.interactionAnnouncements.recordingStarted.ariaLabel', 'Recording started'), }; /** diff --git a/src/components/Attachment/AttachmentActions.tsx b/src/components/Attachment/AttachmentActions.tsx index 0d6f21b429..fb29fc0dfb 100644 --- a/src/components/Attachment/AttachmentActions.tsx +++ b/src/components/Attachment/AttachmentActions.tsx @@ -97,9 +97,9 @@ const UnMemoizedAttachmentActions = (props: AttachmentActionsProps) => { const knownActionText = useMemo>( () => ({ - Cancel: t('Cancel'), - Send: t('Send'), - Shuffle: t('Shuffle'), + Cancel: t('common.cancel.label', 'Cancel'), + Send: t('common.send.label', 'Send'), + Shuffle: t('attachment.actions.shuffle.label', 'Shuffle'), }), [t], ); @@ -143,7 +143,10 @@ const UnMemoizedAttachmentActions = (props: AttachmentActionsProps) => { }, [announceInteraction, descriptiveTitle, giphyImageIdentity, isGiphy]); const groupProps = isGiphy - ? { 'aria-label': t('aria/Giphy actions'), role: 'group' as const } + ? { + 'aria-label': t('attachment.actions.giphyActions.ariaLabel', 'Giphy actions'), + role: 'group' as const, + } : {}; return ( @@ -189,7 +192,8 @@ const UnMemoizedAttachmentActions = (props: AttachmentActionsProps) => { {isGiphy && ( {t( - 'aria/Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.', + 'attachment.actions.giphyPreviewOnlyVisible.ariaLabel', + 'Giphy preview, only visible to you. Use the Send, Shuffle, or Cancel actions.', )} )} diff --git a/src/components/Attachment/Geolocation.tsx b/src/components/Attachment/Geolocation.tsx index 9f73f905a8..1c6f1ad0f2 100644 --- a/src/components/Attachment/Geolocation.tsx +++ b/src/components/Attachment/Geolocation.tsx @@ -57,7 +57,10 @@ export const Geolocation = ({
{isLiveLocation ? ( stoppedSharing ? ( - t('Location sharing ended') + t( + 'attachment.geolocation.locationSharingEnded.text', + 'Location sharing ended', + ) ) : isMyLocation ? (
- {t('Live until {{ timestamp }}', { - timestamp: t('timestamp/LiveLocation', { timestamp: location.end_at }), - })} + {t( + 'attachment.geolocation.liveUntil.text', + 'Live until {{ timestamp }}', + { + timestamp: t('timestamp.LiveLocation', { + timestamp: location.end_at, + }), + }, + )}
) : (
- {t('Live location')} + {t('common.liveLocation.text', 'Live location')}
- {t('Live until {{ timestamp }}', { - timestamp: t('timestamp/LiveLocation', { timestamp: location.end_at }), - })} + {t( + 'attachment.geolocation.liveUntil.text', + 'Live until {{ timestamp }}', + { + timestamp: t('timestamp.LiveLocation', { + timestamp: location.end_at, + }), + }, + )}
) ) : ( - t('Current location') + t('common.currentLocation.text', 'Current location') )}
@@ -111,7 +126,10 @@ const DefaultGeolocationAttachmentMapPlaceholder = ({ > { // localized generic label instead of exposing the URL as the accessible name. const descriptiveTitle = getGiphyDescriptiveTitle(title); const accessibleName = descriptiveTitle - ? t('aria/Animated GIF: {{ title }}', { title: descriptiveTitle }) - : t('aria/Animated GIF'); + ? t('attachment.giphy.animatedGif.withTitle.ariaLabel', 'Animated GIF: {{ title }}', { + title: descriptiveTitle, + }) + : t('attachment.giphy.animatedGif.ariaLabel', 'Animated GIF'); const imageStyleVariables = useMemo(() => { const originalHeight = Number(dimensions?.height); const originalWidth = Number(dimensions?.width); diff --git a/src/components/Attachment/LinkPreview/UnableToRenderCard.tsx b/src/components/Attachment/LinkPreview/UnableToRenderCard.tsx index 2d17e3ce1e..7c559c8388 100644 --- a/src/components/Attachment/LinkPreview/UnableToRenderCard.tsx +++ b/src/components/Attachment/LinkPreview/UnableToRenderCard.tsx @@ -14,7 +14,7 @@ export const UnableToRenderCard = ({ type }: { type?: Attachment['type'] }) => { >
- {t('this content could not be displayed')} + {t('attachment.unableRenderCard.text', 'this content could not be displayed')}
diff --git a/src/components/Attachment/ModalGallery.tsx b/src/components/Attachment/ModalGallery.tsx index 3557c68be7..907d39fc50 100644 --- a/src/components/Attachment/ModalGallery.tsx +++ b/src/components/Attachment/ModalGallery.tsx @@ -172,7 +172,7 @@ const ThumbnailButton = ({ }; const buttonLabel = showRetryIndicator - ? t('aria/Retry upload') + ? t('common.retryUpload.ariaLabel', 'Retry upload') : itemCountAwareLabel({ imageIndex: index + 1, itemCount, t }); return ( @@ -186,11 +186,14 @@ const ThumbnailButton = ({ type='button' > {item.videoThumbnailUrl ? ( - + ) : ( { setIsImageLoading(false); setIsLoadFailed(true); @@ -242,10 +245,14 @@ const itemCountAwareLabel = ({ t: ReturnType['t']; }) => itemCount === 1 - ? t('Open image in gallery') - : t('Open gallery at image {{ index }}', { - index: imageIndex, - }); + ? t('attachment.modalGallery.openImageGallery.label', 'Open image in gallery') + : t( + 'attachment.modalGallery.openGalleryImage.label', + 'Open gallery at image {{ index }}', + { + index: imageIndex, + }, + ); const getBaseImageProps = (item: GalleryItem): BaseImagePropsWithoutSrc => { const baseImageProps: PartialBaseImagePropMap = {}; diff --git a/src/components/Attachment/UnsupportedAttachment.tsx b/src/components/Attachment/UnsupportedAttachment.tsx index b91fcb6d17..fd2c4dd712 100644 --- a/src/components/Attachment/UnsupportedAttachment.tsx +++ b/src/components/Attachment/UnsupportedAttachment.tsx @@ -20,7 +20,7 @@ export const UnsupportedAttachment = () => { className='str-chat__message-attachment-unsupported__title' data-testid='unsupported-attachment-title' > - {t('Unsupported attachment')} + {t('common.unsupportedAttachment.text', 'Unsupported attachment')} diff --git a/src/components/Attachment/VisibilityDisclaimer.tsx b/src/components/Attachment/VisibilityDisclaimer.tsx index 0f58095588..a3b5aabeb3 100644 --- a/src/components/Attachment/VisibilityDisclaimer.tsx +++ b/src/components/Attachment/VisibilityDisclaimer.tsx @@ -7,7 +7,7 @@ export const VisibilityDisclaimer = () => { return (
- {t('Only visible to you')} + {t('attachment.visibilityDisclaimer.onlyVisible.text', 'Only visible to you')}
); }; diff --git a/src/components/Attachment/VoiceRecording.tsx b/src/components/Attachment/VoiceRecording.tsx index 13f667cb16..622ec242cd 100644 --- a/src/components/Attachment/VoiceRecording.tsx +++ b/src/components/Attachment/VoiceRecording.tsx @@ -81,7 +81,7 @@ const VoiceRecordingPlayerUI = ({ audioPlayer }: VoiceRecordingPlayerUIProps) =>
{ const { t } = useTranslationContext(); - const { asset_url, title = t('Voice message') } = attachment; + const { asset_url, title = t('common.voiceMessage.label', 'Voice message') } = + attachment; const { duration = 0, file_size, diff --git a/src/components/Attachment/__tests__/AttachmentActions.test.tsx b/src/components/Attachment/__tests__/AttachmentActions.test.tsx index e633bb5878..1ebe53e292 100644 --- a/src/components/Attachment/__tests__/AttachmentActions.test.tsx +++ b/src/components/Attachment/__tests__/AttachmentActions.test.tsx @@ -19,7 +19,27 @@ vi.mock('../../Accessibility', () => ({ })); vi.mock('../../../context', () => ({ - useTranslationContext: () => ({ t: (key) => key.replace(/^aria\//, '') }), + useTranslationContext: () => ({ + t: (key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }, + }), })); const getComponent = (props) => ; diff --git a/src/components/Attachment/__tests__/Audio.test.tsx b/src/components/Attachment/__tests__/Audio.test.tsx index d8643599a2..d56a4bf61f 100644 --- a/src/components/Attachment/__tests__/Audio.test.tsx +++ b/src/components/Attachment/__tests__/Audio.test.tsx @@ -22,7 +22,7 @@ vi.mock('../../../context/ChatContext', () => ({ useChatContext: () => ({ client: mockClient }), })); vi.mock('../../../context/TranslationContext', () => ({ - useTranslationContext: () => ({ t: (s) => tSpy(s) }), + useTranslationContext: () => ({ t: (...args) => tSpy(...args) }), })); vi.mock('../../Notifications', () => ({ useNotificationApi: () => ({ @@ -35,7 +35,9 @@ vi.mock('../../Notifications', () => ({ const mockClient = { notifications: { add: addNotificationSpy }, }; -const tSpy = (s) => s; +// Mirrors i18next: render the inline English defaultValue when one is given. +const tSpy = (key, defaultValue) => + typeof defaultValue === 'string' ? defaultValue : key; // capture created Audio() elements so we can assert src & dispatch events const createdAudios = []; //HTMLAudioElement[] diff --git a/src/components/Attachment/__tests__/Giphy.test.tsx b/src/components/Attachment/__tests__/Giphy.test.tsx index d6dee86402..27eeb75da5 100644 --- a/src/components/Attachment/__tests__/Giphy.test.tsx +++ b/src/components/Attachment/__tests__/Giphy.test.tsx @@ -15,15 +15,25 @@ vi.mock('../../../context', () => ({ useChannelStateContext: () => channelStateMock, useComponentContext: () => ({}), useTranslationContext: () => ({ - t: (key, params) => - Object.keys(params ?? {}).reduce( - (acc, paramKey) => - acc.replace( - new RegExp(`\\{\\{\\s${paramKey}\\s\\}\\}`, 'g'), - String(params?.[paramKey]), - ), - key.replace(/^aria\//, ''), - ), + t: (key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }, }), })); diff --git a/src/components/Attachment/__tests__/WaveProgressBar.test.tsx b/src/components/Attachment/__tests__/WaveProgressBar.test.tsx index 9d3a15f2e1..d76d1a5e2a 100644 --- a/src/components/Attachment/__tests__/WaveProgressBar.test.tsx +++ b/src/components/Attachment/__tests__/WaveProgressBar.test.tsx @@ -133,15 +133,12 @@ describe('WaveProgressBar', () => { const root = screen.getByTestId(BAR_ROOT_TEST_ID); expect(root).toHaveAttribute('role', 'slider'); - expect(root).toHaveAttribute('aria-label', 'aria/Seek audio position'); + expect(root).toHaveAttribute('aria-label', 'Seek audio position'); expect(root).toHaveAttribute('tabindex', '0'); expect(root).toHaveAttribute('aria-valuemin', '0'); expect(root).toHaveAttribute('aria-valuemax', '100'); expect(root).toHaveAttribute('aria-valuenow', '20'); - expect(root).toHaveAttribute( - 'aria-valuetext', - 'aria/Audio position {{ progress }} percent', - ); + expect(root).toHaveAttribute('aria-valuetext', 'Audio position 20 percent'); fireEvent.keyDown(root, { key: 'End' }); diff --git a/src/components/Attachment/components/DownloadButton.tsx b/src/components/Attachment/components/DownloadButton.tsx index 73c150dff0..a58b199a3b 100644 --- a/src/components/Attachment/components/DownloadButton.tsx +++ b/src/components/Attachment/components/DownloadButton.tsx @@ -32,7 +32,7 @@ export const DownloadButton = ({ return (
diff --git a/src/components/AudioPlayback/__tests__/ProgressBar.test.tsx b/src/components/AudioPlayback/__tests__/ProgressBar.test.tsx index bf2462569b..345c875bda 100644 --- a/src/components/AudioPlayback/__tests__/ProgressBar.test.tsx +++ b/src/components/AudioPlayback/__tests__/ProgressBar.test.tsx @@ -9,15 +9,12 @@ describe('ProgressBar', () => { const root = screen.getByTestId('audio-progress'); expect(root).toHaveAttribute('role', 'slider'); - expect(root).toHaveAttribute('aria-label', 'aria/Seek audio position'); + expect(root).toHaveAttribute('aria-label', 'Seek audio position'); expect(root).toHaveAttribute('tabindex', '0'); expect(root).toHaveAttribute('aria-valuemin', '0'); expect(root).toHaveAttribute('aria-valuemax', '100'); expect(root).toHaveAttribute('aria-valuenow', '40'); - expect(root).toHaveAttribute( - 'aria-valuetext', - 'aria/Audio position {{ progress }} percent', - ); + expect(root).toHaveAttribute('aria-valuetext', 'Audio position 40 percent'); }); it('seeks forward with ArrowRight key', () => { @@ -71,9 +68,6 @@ describe('ProgressBar', () => { ); const root = screen.getByTestId('audio-progress'); - expect(root).toHaveAttribute( - 'aria-valuetext', - 'aria/Audio position {{ elapsed }} of {{ duration }}', - ); + expect(root).toHaveAttribute('aria-valuetext', 'Audio position 01:20 of 03:20'); }); }); diff --git a/src/components/AudioPlayback/__tests__/WithAudioPlayback.test.tsx b/src/components/AudioPlayback/__tests__/WithAudioPlayback.test.tsx index af153896b6..0701092f59 100644 --- a/src/components/AudioPlayback/__tests__/WithAudioPlayback.test.tsx +++ b/src/components/AudioPlayback/__tests__/WithAudioPlayback.test.tsx @@ -11,7 +11,25 @@ const { mockAddNotification } = vi.hoisted(() => ({ // mock context used by WithAudioPlayback vi.mock('../../../context', () => { - const t = (s: string) => s; + const t = (key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }; return { __esModule: true, useTranslationContext: () => ({ t }), diff --git a/src/components/AudioPlayback/components/ProgressBar.tsx b/src/components/AudioPlayback/components/ProgressBar.tsx index 9aa9568ff4..bae689479c 100644 --- a/src/components/AudioPlayback/components/ProgressBar.tsx +++ b/src/components/AudioPlayback/components/ProgressBar.tsx @@ -44,7 +44,10 @@ export const ProgressBar = ({ return (
{ const errors: Record = { - 'failed-to-start': new Error(t('Failed to play the recording')), + 'failed-to-start': new Error( + t( + 'audioPlayback.audioPlayerNotifications.failedPlayRecording.label', + 'Failed to play the recording', + ), + ), 'not-playable': new Error( - t('Recording format is not supported and cannot be reproduced'), + t( + 'audioPlayback.audioPlayerNotifications.recordingFormatNotSupported.label', + 'Recording format is not supported and cannot be reproduced', + ), + ), + 'seek-not-supported': new Error( + t( + 'audioPlayback.audioPlayerNotifications.cannotSeekRecording.label', + 'Cannot seek in the recording', + ), ), - 'seek-not-supported': new Error(t('Cannot seek in the recording')), }; let lastSeekNotSupportedNotificationAt: number | undefined; @@ -44,7 +57,9 @@ export const audioPlayerNotificationsPluginFactory = ({ const error = (errCode && errors[errCode]) ?? e ?? - new Error(t('Error reproducing the recording')); + new Error( + t('notification.audioPlaybackError', 'Error reproducing the recording'), + ); addNotification({ emitter: 'AudioPlayer', diff --git a/src/components/AudioPlayback/plugins/__tests__/AudioPlayerNotificationsPlugin.test.ts b/src/components/AudioPlayback/plugins/__tests__/AudioPlayerNotificationsPlugin.test.ts index bd887e18cc..bf50b2f2b0 100644 --- a/src/components/AudioPlayback/plugins/__tests__/AudioPlayerNotificationsPlugin.test.ts +++ b/src/components/AudioPlayback/plugins/__tests__/AudioPlayerNotificationsPlugin.test.ts @@ -1,9 +1,10 @@ import { fromPartial } from '@total-typescript/shoehorn'; import type { TFunction } from 'i18next'; import { audioPlayerNotificationsPluginFactory } from '../AudioPlayerNotificationsPlugin'; +import { mockT } from '../../../../mock-builders/translator'; describe('audioPlayerNotificationsPluginFactory', () => { - const t: TFunction = ((s: string) => s) as TFunction; + const t: TFunction = mockT as TFunction; const makeNotifier = () => { const addNotification = vi.fn(); diff --git a/src/components/BaseImage/ImagePlaceholder.tsx b/src/components/BaseImage/ImagePlaceholder.tsx index af2b99c4d9..c1a5f6c6f6 100644 --- a/src/components/BaseImage/ImagePlaceholder.tsx +++ b/src/components/BaseImage/ImagePlaceholder.tsx @@ -11,7 +11,10 @@ export const ImagePlaceholder = ({ className }: ImagePlaceholderProps) => { const { t } = useTranslationContext(); return (
-
{t('Channel Missing')}
+
{t('channel.channelMissing.text', 'Channel Missing')}
); } diff --git a/src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts b/src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts index 99b5873dd5..e670b8c438 100644 --- a/src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts +++ b/src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts @@ -37,8 +37,10 @@ export function useChannelHeaderOnlineStatus(): string | null { if (!memberCount) return null; if (isDirectMessagingChannel) { - return hasMembersOnline ? t('Online') : t('Offline'); + return hasMembersOnline + ? t('common.online.label', 'Online') + : t('common.offline.label', 'Offline'); } - return `${t('{{ memberCount }} members', { memberCount })} · ${t('{{ watcherCount }} online', { watcherCount })}`; + return `${t('channelHeader.online.members.label', '{{ memberCount }} members', { memberCount })} · ${t('channelHeader.online.online.label', '{{ watcherCount }} online', { watcherCount })}`; } diff --git a/src/components/ChannelList/ChannelList.tsx b/src/components/ChannelList/ChannelList.tsx index fb3ad6c153..83d4573b1b 100644 --- a/src/components/ChannelList/ChannelList.tsx +++ b/src/components/ChannelList/ChannelList.tsx @@ -80,7 +80,7 @@ export const ChannelList = ({ return ( - aria-label={t('aria/Channel list')} + aria-label={t('channelList.channelList.ariaLabel', 'Channel list')} contentProps={{ role: 'presentation' }} EmptyListIndicator={EmptyListIndicator} EndReachedIndicator={EndReachedIndicator} diff --git a/src/components/ChannelList/ChannelListHeader.tsx b/src/components/ChannelList/ChannelListHeader.tsx index 6d65281e44..eac51f0012 100644 --- a/src/components/ChannelList/ChannelListHeader.tsx +++ b/src/components/ChannelList/ChannelListHeader.tsx @@ -12,7 +12,9 @@ export const ChannelListHeader = () => { const hasActiveChannel = useWorkspaceNavigation().openChannels.length > 0; return (
-
{t('Chats')}
+
+ {t('channelList.header.chats.text', 'Chats')} +
{hasActiveChannel && HeaderEndContent && }
); diff --git a/src/components/ChannelList/__tests__/ChannelListHeader.test.tsx b/src/components/ChannelList/__tests__/ChannelListHeader.test.tsx index 8d01a67d4d..7062002be6 100644 --- a/src/components/ChannelList/__tests__/ChannelListHeader.test.tsx +++ b/src/components/ChannelList/__tests__/ChannelListHeader.test.tsx @@ -5,8 +5,9 @@ import { WithComponents, WorkspaceNavigationProvider } from '../../../context'; import { TranslationProvider } from '../../../context/TranslationContext'; import { mockTranslationContextValue } from '../../../mock-builders'; import { ChannelListHeader } from '../ChannelListHeader'; +import { mockT } from '../../../mock-builders/translator'; -const t = vi.fn((key: string) => key); +const t = vi.fn(mockT); const HeaderEndContent = () =>
; afterEach(cleanup); diff --git a/src/components/ChannelListItem/ChannelListItemActionButtons.defaults.tsx b/src/components/ChannelListItem/ChannelListItemActionButtons.defaults.tsx index d73b16e8f6..e40cca3f51 100644 --- a/src/components/ChannelListItem/ChannelListItemActionButtons.defaults.tsx +++ b/src/components/ChannelListItem/ChannelListItemActionButtons.defaults.tsx @@ -51,7 +51,7 @@ const useMuteAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('Channel unmuted'), + message: t('common.channelUnmuted.text', 'Channel unmuted'), severity: 'success', type: 'api:channel:unmute:success', }); @@ -60,7 +60,7 @@ const useMuteAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('Channel muted'), + message: t('common.channelMuted.text', 'Channel muted'), severity: 'success', type: 'api:channel:mute:success', }); @@ -70,14 +70,21 @@ const useMuteAction = (): ChannelActionBehavior => { context: { channel }, emitter: ChannelListItemActionButtons.name, error: error instanceof Error ? error : new Error('An unknown error occurred'), - message: t('Failed to update channel mute status'), + message: t( + 'channelListItem.failedUpdateChannelMute.text', + 'Failed to update channel mute status', + ), severity: 'error', type: 'api:channel:mute:failed', }); } }; - return { 'aria-pressed': isMuted, title: isMuted ? t('Unmute') : t('Mute'), toggle }; + return { + 'aria-pressed': isMuted, + title: isMuted ? t('common.unmute.title', 'Unmute') : t('common.mute.title', 'Mute'), + toggle, + }; }; // Core archive/unarchive action — performs the API call and reports the result. @@ -94,7 +101,7 @@ const useArchiveAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('Channel unarchived'), + message: t('channelListItem.channelUnarchived.text', 'Channel unarchived'), severity: 'success', type: 'api:channel:unarchive:success', }); @@ -103,7 +110,7 @@ const useArchiveAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('Channel archived'), + message: t('channelListItem.channelArchived.text', 'Channel archived'), severity: 'success', type: 'api:channel:archive:success', }); @@ -113,7 +120,10 @@ const useArchiveAction = (): ChannelActionBehavior => { context: { channel }, emitter: ChannelListItemActionButtons.name, error: error instanceof Error ? error : new Error('An unknown error occurred'), - message: t('Failed to update channel archive status'), + message: t( + 'channelListItem.failedUpdateChannelArchive.text', + 'Failed to update channel archive status', + ), severity: 'error', type: 'api:channel:archive:failed', }); @@ -122,7 +132,9 @@ const useArchiveAction = (): ChannelActionBehavior => { return { 'aria-pressed': typeof membership.archived_at === 'string', - title: membership.archived_at ? t('Unarchive') : t('Archive'), + title: membership.archived_at + ? t('channelListItem.unarchive.title', 'Unarchive') + : t('channelListItem.archive.title', 'Archive'), toggle, }; }; @@ -213,7 +225,7 @@ const useBanAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('User unblocked'), + message: t('common.userUnblocked.text', 'User unblocked'), severity: 'success', type: 'api:user:unban:success', }); @@ -222,7 +234,7 @@ const useBanAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('User blocked'), + message: t('common.userBlocked.text', 'User blocked'), severity: 'success', type: 'api:user:ban:success', }); @@ -232,7 +244,7 @@ const useBanAction = (): ChannelActionBehavior => { context: { channel }, emitter: ChannelListItemActionButtons.name, error: error instanceof Error ? error : new Error('An unknown error occurred'), - message: t('Failed to block user'), + message: t('channelListItem.failedBlockUser.text', 'Failed to block user'), severity: 'error', type: 'api:user:ban:failed', }); @@ -241,7 +253,9 @@ const useBanAction = (): ChannelActionBehavior => { return { 'aria-pressed': isUserBanned, - title: isUserBanned ? t('Unblock User') : t('Block User'), + title: isUserBanned + ? t('channelListItem.unblockUser.title', 'Unblock User') + : t('common.blockUser.title', 'Block User'), toggle, }; }; @@ -260,7 +274,7 @@ const useLeaveAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('Left channel'), + message: t('common.leftChannel.text', 'Left channel'), severity: 'success', type: 'api:channel:leave:success', }); @@ -269,14 +283,14 @@ const useLeaveAction = (): ChannelActionBehavior => { context: { channel }, emitter: ChannelListItemActionButtons.name, error: error instanceof Error ? error : new Error('An unknown error occurred'), - message: t('Failed to leave channel'), + message: t('common.failedLeaveChannel.text', 'Failed to leave channel'), severity: 'error', type: 'api:channel:leave:failed', }); } }; - return { title: t('Leave Channel'), toggle }; + return { title: t('channelListItem.leaveChannel.title', 'Leave Channel'), toggle }; }; // Core pin/unpin action. @@ -293,7 +307,7 @@ const usePinAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('Channel unpinned'), + message: t('channelListItem.channelUnpinned.text', 'Channel unpinned'), severity: 'success', type: 'api:channel:unpin:success', }); @@ -302,7 +316,7 @@ const usePinAction = (): ChannelActionBehavior => { addNotification({ context: { channel }, emitter: ChannelListItemActionButtons.name, - message: t('Channel pinned'), + message: t('channelListItem.channelPinned.text', 'Channel pinned'), severity: 'success', type: 'api:channel:pin:success', }); @@ -312,7 +326,10 @@ const usePinAction = (): ChannelActionBehavior => { context: { channel }, emitter: ChannelListItemActionButtons.name, error: error instanceof Error ? error : new Error('An unknown error occurred'), - message: t('Failed to update channel pinned status'), + message: t( + 'channelListItem.failedUpdateChannelPinned.text', + 'Failed to update channel pinned status', + ), severity: 'error', type: 'api:channel:pin:failed', }); @@ -321,7 +338,9 @@ const usePinAction = (): ChannelActionBehavior => { return { 'aria-pressed': !!membership.pinned_at, - title: membership.pinned_at ? t('Unpin') : t('Pin'), + title: membership.pinned_at + ? t('common.unpin.title', 'Unpin') + : t('common.pin.title', 'Pin'), toggle, }; }; @@ -461,7 +480,10 @@ const defaultComponents = { diff --git a/src/components/Gallery/GalleryUI.tsx b/src/components/Gallery/GalleryUI.tsx index 4112fd4152..e0a1688b95 100644 --- a/src/components/Gallery/GalleryUI.tsx +++ b/src/components/Gallery/GalleryUI.tsx @@ -200,7 +200,7 @@ export const GalleryUI = () => {
{
) => { const { t } = useTranslationContext('UnMemoizedLoadMoreButton'); - const childrenOrDefaultString = children ?? t('Load more'); + const childrenOrDefaultString = + children ?? t('loadMore.button.loadMore.label', 'Load more'); return (
; + return ( +
+ {t('loading.errorIndicator.error.text', 'Error: {{ errorMessage }}', { + errorMessage: error.message, + })} +
+ ); }; export const LoadingErrorIndicator = React.memo( diff --git a/src/components/Loading/__tests__/LoadingErrorIndicator.test.tsx b/src/components/Loading/__tests__/LoadingErrorIndicator.test.tsx index c7a36dfa3d..dfbdac8e39 100644 --- a/src/components/Loading/__tests__/LoadingErrorIndicator.test.tsx +++ b/src/components/Loading/__tests__/LoadingErrorIndicator.test.tsx @@ -26,7 +26,7 @@ describe('LoadingErrorIndicator', () => { expect(container).toMatchInlineSnapshot(`
- Error: {{ errorMessage }} + Error: this is an error
`); diff --git a/src/components/Loading/progress-indicators.tsx b/src/components/Loading/progress-indicators.tsx index ccc53db25b..21b694f574 100644 --- a/src/components/Loading/progress-indicators.tsx +++ b/src/components/Loading/progress-indicators.tsx @@ -18,7 +18,11 @@ export const CircularProgressIndicator = ({ percent }: ProgressIndicatorProps) = return (
durations.length > 0 - ? t('duration/Share Location', { + ? t('duration.shareLocation', { milliseconds: selectedDuration ?? durations[0], }) : undefined, @@ -153,9 +153,10 @@ export const ShareLocationDialog = ({ {liveLocationSwitchEnabled && selectedDurationLabel && (
@@ -214,7 +218,7 @@ export const ShareLocationDialog = ({ close(); }} > - {t('Cancel')} + {t('common.cancel.label', 'Cancel')} - {t('Attach')} + {t('location.shareLocationDialog.attach.text', 'Attach')} - {t('Share')} + {t('location.shareLocationDialog.share.text', 'Share')} @@ -314,7 +324,7 @@ const DurationDropdownItems = ({ }} role='menuitemradio' > - {t('duration/Share Location', { milliseconds: duration })} + {t('duration.shareLocation', { milliseconds: duration })} ))} diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx index a3b96cb522..a0c758d08f 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx @@ -18,7 +18,17 @@ const ToggleRecordingButton = () => { return (
); diff --git a/src/components/Message/MessageBlocked.tsx b/src/components/Message/MessageBlocked.tsx index 3a71387083..d24112e1fd 100644 --- a/src/components/Message/MessageBlocked.tsx +++ b/src/components/Message/MessageBlocked.tsx @@ -27,7 +27,7 @@ export const MessageBlocked = () => { key={message.id} >
- {t('Message was blocked by moderation policies')} + {t('message.blocked.text', 'Message was blocked by moderation policies')}
); diff --git a/src/components/Message/MessageDeletedBubble.tsx b/src/components/Message/MessageDeletedBubble.tsx index ab2e86e4bf..8d3103feb1 100644 --- a/src/components/Message/MessageDeletedBubble.tsx +++ b/src/components/Message/MessageDeletedBubble.tsx @@ -17,7 +17,7 @@ export const MessageDeletedBubble = () => {
- {t('Message deleted')} + {t('common.messageDeleted.text', 'Message deleted')}
); diff --git a/src/components/Message/MessageEditedIndicator.tsx b/src/components/Message/MessageEditedIndicator.tsx index d8d37d2027..5daf71025a 100644 --- a/src/components/Message/MessageEditedIndicator.tsx +++ b/src/components/Message/MessageEditedIndicator.tsx @@ -40,7 +40,7 @@ const UnMemoizedMessageEditedIndicator = (props: MessageEditedIndicatorProps) => onMouseLeave={handleLeave} ref={setReferenceElement} > - {t('Edited')} + {t('message.editedIndicator.edited.text', 'Edited')} 1) { replyCountText = `${replyCount} ${labelPlural}`; diff --git a/src/components/Message/MessageStatus.tsx b/src/components/Message/MessageStatus.tsx index c49006bfd5..e49ce8b7d2 100644 --- a/src/components/Message/MessageStatus.tsx +++ b/src/components/Message/MessageStatus.tsx @@ -101,7 +101,7 @@ const UnMemoizedMessageStatus = (props: MessageStatusProps) => { referenceElement={referenceElement} visible={tooltipVisible} > - {t('Sending...')} + {t('message.status.sending.text', 'Sending...')} @@ -117,7 +117,7 @@ const UnMemoizedMessageStatus = (props: MessageStatusProps) => { referenceElement={referenceElement} visible={tooltipVisible} > - {t('Sent')} + {t('message.status.sent.text', 'Sent')} @@ -133,7 +133,7 @@ const UnMemoizedMessageStatus = (props: MessageStatusProps) => { referenceElement={referenceElement} visible={tooltipVisible} > - {t('Delivered')} + {t('message.status.delivered.text', 'Delivered')} diff --git a/src/components/Message/MessageText.tsx b/src/components/Message/MessageText.tsx index 94b440699a..507ba6b01b 100644 --- a/src/components/Message/MessageText.tsx +++ b/src/components/Message/MessageText.tsx @@ -83,8 +83,10 @@ const UnMemoizedMessageTextComponent = (props: MessageTextProps) => { hasMentions && typeof onMentionsClickMessage === 'function'; const senderName = message.user?.name; const messageContext = senderName - ? t('aria/Message from {{ user }},', { user: senderName }) - : t('aria/Message,'); + ? t('message.text.message.withUser.ariaLabel', 'Message from {{ user }},', { + user: senderName, + }) + : t('message.text.message.ariaLabel', 'Message,'); // `aria-labelledby` accepts a space-separated list of element ids. We point to the // hidden message context and the rendered message text so screen readers announce both. const messageLabelledBy = `${messageContextId} ${messageTextId}`; diff --git a/src/components/Message/MessageTranslationIndicator.tsx b/src/components/Message/MessageTranslationIndicator.tsx index 1a32cf8a2e..be7998c7c3 100644 --- a/src/components/Message/MessageTranslationIndicator.tsx +++ b/src/components/Message/MessageTranslationIndicator.tsx @@ -50,7 +50,7 @@ export const MessageTranslationIndicator = ({ const sourceLanguageName = useMemo(() => { const sourceLanguageCode = message?.i18n?.language; if (!sourceLanguageCode) return ''; - const languageKey = 'language/' + sourceLanguageCode; + const languageKey = 'language.' + sourceLanguageCode; const translatedName = t(languageKey); return translatedName && translatedName !== languageKey ? translatedName @@ -65,10 +65,14 @@ export const MessageTranslationIndicator = ({ {viewingOriginal - ? t('Original') + ? t('message.translationIndicator.original.text', 'Original') : sourceLanguageName - ? t('Translated from {{ language }}', { language: sourceLanguageName }) - : t('Translated')} + ? t( + 'message.translationIndicator.translated.withLanguage.text', + 'Translated from {{ language }}', + { language: sourceLanguageName }, + ) + : t('message.translationIndicator.translated.text', 'Translated')} · ); diff --git a/src/components/Message/MessageUI.tsx b/src/components/Message/MessageUI.tsx index bad8d48878..96631f0f0e 100644 --- a/src/components/Message/MessageUI.tsx +++ b/src/components/Message/MessageUI.tsx @@ -182,7 +182,7 @@ const MessageUIWithContext = ({ const isMessageInnerInteractive = !!handleClick; const messageInnerAriaLabel = isMessageInnerInteractive - ? t('aria/Review bounced message') + ? t('message.ui.reviewBouncedMessage.ariaLabel', 'Review bounced message') : undefined; const handleMessageInnerKeyDown = (event: React.KeyboardEvent) => { diff --git a/src/components/Message/PinIndicator.tsx b/src/components/Message/PinIndicator.tsx index a8e1b18581..770c9d0664 100644 --- a/src/components/Message/PinIndicator.tsx +++ b/src/components/Message/PinIndicator.tsx @@ -22,10 +22,10 @@ export const PinIndicator = ({ message }: PinIndicatorProps) => { const name = message.pinned_by?.name ?? message.pinned_by?.id ?? ''; const label = isOwnPin - ? t('Pinned by You') + ? t('message.pinIndicator.pinned.label', 'Pinned by You') : name - ? t('Pinned by {{ name }}', { name }) - : t('Message pinned'); + ? t('message.pinIndicator.pinned.withName.label', 'Pinned by {{ name }}', { name }) + : t('common.messagePinned.label', 'Message pinned'); return (
diff --git a/src/components/Message/ReminderNotification.tsx b/src/components/Message/ReminderNotification.tsx index 0b2736ed52..ee1ff72163 100644 --- a/src/components/Message/ReminderNotification.tsx +++ b/src/components/Message/ReminderNotification.tsx @@ -17,7 +17,7 @@ function SavedForLaterContent() { return (
- {t('Saved for later')} + {t('common.savedLater.text', 'Saved for later')}
); } @@ -51,32 +51,40 @@ function RemindMeContent({ reminder }: { reminder: Reminder }) { if (useAbsoluteFormat) { // > 59 min ago: calendar + time (same as DateSeparator + HH:mm) // e.g. "Due since Today at 15:00", "Due since Yesterday at 09:30" - return t('Due since {{ dueSince }}', { - dueSince: t('timestamp/ReminderNotification', { - timestamp: reminder.remindAt, - }), - }); + return t( + 'message.reminderNotification.dueSince.label', + 'Due since {{ dueSince }}', + { + dueSince: t('timestamp.ReminderNotification', { + timestamp: reminder.remindAt, + }), + }, + ); } // Within 59 min ago: relative // e.g. "Due since 5 minutes ago", "Due since a minute ago" - return t('Due since {{ dueSince }}', { - dueSince: t('duration/Message reminder', { - milliseconds: diffMs, - }), - }); + return t( + 'message.reminderNotification.dueSince.label', + 'Due since {{ dueSince }}', + { + dueSince: t('duration.messageReminder', { + milliseconds: diffMs, + }), + }, + ); } // Future: reminder not yet due if (useAbsoluteFormat) { // > 59 min from now: calendar + time (no "Due" prefix) // e.g. "Today at 15:00", "Tomorrow at 09:30" - return t('timestamp/ReminderNotification', { + return t('timestamp.ReminderNotification', { timestamp: reminder.remindAt, }); } // Within 59 min from now: relative // e.g. "Due in 30 minutes", "Due in a minute" - return t('Due {{ timeLeft }}', { - timeLeft: t('duration/Message reminder', { + return t('message.reminderNotification.due.label', 'Due {{ timeLeft }}', { + timeLeft: t('duration.messageReminder', { milliseconds: timeLeftMs, }), }); @@ -85,7 +93,7 @@ function RemindMeContent({ reminder }: { reminder: Reminder }) { return (

- {t('Reminder set')} + {t('common.reminderSet.text', 'Reminder set')} · {renderTime()}

diff --git a/src/components/Message/Timestamp.tsx b/src/components/Message/Timestamp.tsx index 976476eeca..58f4f3fda1 100644 --- a/src/components/Message/Timestamp.tsx +++ b/src/components/Message/Timestamp.tsx @@ -31,7 +31,7 @@ export function Timestamp(props: TimestampProps) { messageCreatedAt: normalizedTimestamp, t, tDateTimeParser, - timestampTranslationKey: 'timestamp/MessageTimestamp', + timestampTranslationKey: 'timestamp.MessageTimestamp', }), [ calendar, diff --git a/src/components/Message/__tests__/MessageDeleted.test.tsx b/src/components/Message/__tests__/MessageDeleted.test.tsx index 1368d15fd1..6adcadda82 100644 --- a/src/components/Message/__tests__/MessageDeleted.test.tsx +++ b/src/components/Message/__tests__/MessageDeleted.test.tsx @@ -5,11 +5,12 @@ import { MessageDeletedBubble } from '../MessageDeletedBubble'; import { TranslationProvider } from '../../../context/TranslationContext'; import { mockTranslationContextValue } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; const messageDeletedTestId = 'message-deleted-bubble'; function renderComponent() { - const t = vi.fn((key) => key); + const t = vi.fn(mockT); return render( diff --git a/src/components/Message/__tests__/MessageStatus.test.tsx b/src/components/Message/__tests__/MessageStatus.test.tsx index 1ef6803c45..920b30909f 100644 --- a/src/components/Message/__tests__/MessageStatus.test.tsx +++ b/src/components/Message/__tests__/MessageStatus.test.tsx @@ -17,6 +17,7 @@ import { mockMessageContext, mockTranslationContextValue, } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; const MESSAGE_STATUS_SENDING_TEST_ID = 'message-status-sending'; const MESSAGE_STATUS_DELIVERED_TEST_ID = 'message-status-delivered'; @@ -46,7 +47,7 @@ const sendingMsg = { ...foreignMsg, status: 'sending', user }; const sentMsg = { ...foreignMsg, user }; const deliveredTo = [otherUser, user]; const readByOthers = [otherUser, user]; -const t = vi.fn((s) => s); +const t = vi.fn(mockT); const defaultMsgCtx = { isMyMessage: vi.fn().mockReturnValue(true), diff --git a/src/components/Message/__tests__/MessageText.test.tsx b/src/components/Message/__tests__/MessageText.test.tsx index ee114b3bb0..f41651be69 100644 --- a/src/components/Message/__tests__/MessageText.test.tsx +++ b/src/components/Message/__tests__/MessageText.test.tsx @@ -31,6 +31,7 @@ import { MessageText } from '../MessageText'; import type { MessageProps } from '../types'; import type { MessageTextProps } from '../MessageText'; import type { TranslationContextValue } from '../../../context'; +import { mockT } from '../../../mock-builders/translator'; vi.mock('../../ChatView', async (importOriginal) => { const actual = await importOriginal(); @@ -60,8 +61,6 @@ const defaultProps = { message: generateMessage(), threadList: false, }; -const translate = (key: string, options?: Record) => - key.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, token: string) => options?.[token] ?? ''); function generateAliceMessage(messageOptions) { return generateMessage({ @@ -87,8 +86,7 @@ async function renderMessageText({ customProps = {} } = {}) { ) => - translate(key, options)) as TranslationContextValue['t'], + t: mockT as TranslationContextValue['t'], tDateTimeParser: customDateTimeParser as TranslationContextValue['tDateTimeParser'], userLanguage: 'en', @@ -301,7 +299,7 @@ describe('', () => { const focusableWrapper = getByTestId(messageTextTestId).parentElement; - expect(focusableWrapper).toHaveAccessibleName(`aria/Message from alice, ${text}`); + expect(focusableWrapper).toHaveAccessibleName(`Message from alice, ${text}`); }); it('should expose sender context on the mention-interactive text wrapper', async () => { @@ -312,7 +310,7 @@ describe('', () => { }); expect(getByTestId(messageTextTestId)).toHaveAccessibleName( - `aria/Message from alice, ${text}`, + `Message from alice, ${text}`, ); }); @@ -328,7 +326,7 @@ describe('', () => { const focusableWrapper = getByTestId(messageTextTestId).parentElement; - expect(focusableWrapper).toHaveAccessibleName(`aria/Message, ${text}`); + expect(focusableWrapper).toHaveAccessibleName(`Message, ${text}`); }); it('should inform that message was not sent when message is has type "error"', async () => { diff --git a/src/components/Message/__tests__/MessageTimestamp.test.tsx b/src/components/Message/__tests__/MessageTimestamp.test.tsx index 95ce34e20a..891d7949af 100644 --- a/src/components/Message/__tests__/MessageTimestamp.test.tsx +++ b/src/components/Message/__tests__/MessageTimestamp.test.tsx @@ -132,7 +132,7 @@ describe('', () => { chatProps: { i18nInstance: new Streami18n({ translationsForLanguage: { - 'timestamp/MessageTimestamp': + 'timestamp.MessageTimestamp': '{{ timestamp | timestampFormatter(calendar: false) }}', }, }), @@ -146,7 +146,7 @@ describe('', () => { chatProps: { i18nInstance: new Streami18n({ translationsForLanguage: { - 'timestamp/MessageTimestamp': + 'timestamp.MessageTimestamp': '{{ timestamp | timestampFormatter(format: h:mmA) }}', }, }), @@ -169,7 +169,7 @@ describe('', () => { chatProps: { i18nInstance: new Streami18n({ translationsForLanguage: { - 'timestamp/MessageTimestamp': + 'timestamp.MessageTimestamp': '{{ timestamp | timestampFormatter(format: h:mmA) }}', }, }), @@ -186,7 +186,7 @@ describe('', () => { chatProps: { i18nInstance: new Streami18n({ translationsForLanguage: { - 'timestamp/MessageTimestamp': + 'timestamp.MessageTimestamp': '{{ timestamp | timestampFormatter(calendar: false) }}', }, }), @@ -208,7 +208,7 @@ describe('', () => { chatProps: { i18nInstance: new Streami18n({ translationsForLanguage: { - 'timestamp/MessageTimestamp': + 'timestamp.MessageTimestamp': '{{ timestamp | timestampFormatter(format: h:mmA) }}', }, }), diff --git a/src/components/Message/__tests__/QuotedMessage.test.tsx b/src/components/Message/__tests__/QuotedMessage.test.tsx index 7bcbd4fa70..df2254a1fa 100644 --- a/src/components/Message/__tests__/QuotedMessage.test.tsx +++ b/src/components/Message/__tests__/QuotedMessage.test.tsx @@ -23,6 +23,7 @@ import { Message } from '../Message'; import { MessageUI } from '../MessageUI'; import { QuotedMessage } from '../QuotedMessage'; import { renderText } from '../renderText'; +import { mockT } from '../../../mock-builders/translator'; vi.mock('../../ChatView', async (importOriginal) => { const actual = await importOriginal(); @@ -68,7 +69,7 @@ async function renderQuotedMessage({ key, + t: mockT, tDateTimeParser: customDateTimeParser, userLanguage: 'en', })} @@ -248,10 +249,7 @@ describe('QuotedMessage', () => { }); const quotedMessagePreview = getByTestId(quotedMessagePreviewTestId); - expect(quotedMessagePreview).toHaveAttribute( - 'aria-label', - 'aria/Jump to quoted message', - ); + expect(quotedMessagePreview).toHaveAttribute('aria-label', 'Jump to quoted message'); expect(quotedMessagePreview).toHaveAttribute('role', 'button'); expect(quotedMessagePreview).toHaveAttribute('tabindex', '0'); }); diff --git a/src/components/Message/__tests__/utils.test.ts b/src/components/Message/__tests__/utils.test.ts index 0fbde731b4..166920e3fc 100644 --- a/src/components/Message/__tests__/utils.test.ts +++ b/src/components/Message/__tests__/utils.test.ts @@ -12,7 +12,7 @@ import { countReactions, getTestClientWithUser, groupReactions, - mockTranslatorFunction, + mockT, } from '../../../mock-builders'; import { areMessagePropsEqual, @@ -463,7 +463,7 @@ describe('Message utils', () => { it('ignores the client user', () => { const result = getReadByTooltipText( [client.user], - mockTranslatorFunction as unknown as TFunction, + mockT as unknown as TFunction, client, tooltipUserNameMapper, ); @@ -472,7 +472,7 @@ describe('Message utils', () => { it('returns a single user if only one user in array', () => { const result = getReadByTooltipText( [bob], - mockTranslatorFunction as unknown as TFunction, + mockT as unknown as TFunction, client, tooltipUserNameMapper, ); @@ -482,7 +482,7 @@ describe('Message utils', () => { const users = [generateUser({ name: '1' }), generateUser({ name: '2' })]; const result = getReadByTooltipText( users, - mockTranslatorFunction as unknown as TFunction, + mockT as unknown as TFunction, client, tooltipUserNameMapper, ); @@ -496,7 +496,7 @@ describe('Message utils', () => { ]; const result = getReadByTooltipText( users, - mockTranslatorFunction as unknown as TFunction, + mockT as unknown as TFunction, client, tooltipUserNameMapper, ); @@ -508,7 +508,7 @@ describe('Message utils', () => { ); const result = getReadByTooltipText( users, - mockTranslatorFunction as unknown as TFunction, + mockT as unknown as TFunction, client, tooltipUserNameMapper, ); @@ -518,7 +518,7 @@ describe('Message utils', () => { const users = [generateUser({ name: '1' }), generateUser({ name: '2' })]; const result = getReadByTooltipText( users, - mockTranslatorFunction as unknown as TFunction, + mockT as unknown as TFunction, client, (user) => `Dr. ${user.name}`, ); @@ -540,7 +540,7 @@ describe('Message utils', () => { expect(() => getReadByTooltipText( [], - mockTranslatorFunction as unknown as TFunction, + mockT as unknown as TFunction, client, undefined as unknown as typeof tooltipUserNameMapper, ), diff --git a/src/components/Message/hooks/__tests__/useMessageAlsoSentInChannelNavigation.test.tsx b/src/components/Message/hooks/__tests__/useMessageAlsoSentInChannelNavigation.test.tsx index 191fd82bb1..37460bd62f 100644 --- a/src/components/Message/hooks/__tests__/useMessageAlsoSentInChannelNavigation.test.tsx +++ b/src/components/Message/hooks/__tests__/useMessageAlsoSentInChannelNavigation.test.tsx @@ -35,7 +35,27 @@ vi.mock('../../../../context', () => ({ }, }), useMessageContext: () => ({ message: mocks.state.message }), - useTranslationContext: () => ({ t: (key: string) => key }), + useTranslationContext: () => ({ + t: (key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }, + }), // `isChannelActive` mirrors the old `useSlotForKey` presence check: truthy when the channel // is already shown in the workspace. useWorkspaceNavigation: () => ({ diff --git a/src/components/Message/hooks/useDeleteHandler.ts b/src/components/Message/hooks/useDeleteHandler.ts index f7ac3fb960..047fa64fb0 100644 --- a/src/components/Message/hooks/useDeleteHandler.ts +++ b/src/components/Message/hooks/useDeleteHandler.ts @@ -47,7 +47,12 @@ export const useDeleteHandler = ( const errorMessage = getErrorNotification && validateAndGetMessage(getErrorNotification, [message]); - if (notify) notify(errorMessage || t('Error deleting message'), 'error'); + if (notify) + notify( + errorMessage || + t('common.errorDeletingMessage.label', 'Error deleting message'), + 'error', + ); } }; }; diff --git a/src/components/Message/hooks/useMessageAlsoSentInChannelNavigation.ts b/src/components/Message/hooks/useMessageAlsoSentInChannelNavigation.ts index 8281d7bb49..587f5dbb80 100644 --- a/src/components/Message/hooks/useMessageAlsoSentInChannelNavigation.ts +++ b/src/components/Message/hooks/useMessageAlsoSentInChannelNavigation.ts @@ -39,7 +39,7 @@ export const useMessageAlsoSentInChannelNavigation = const addThreadNotFoundNotification = (error: Error) => { client.notifications.addError({ - message: t('Thread has not been found'), + message: t('notification.replySearchFailed', 'Thread has not been found'), options: { originalError: error, type: 'api:message:search:not-found', diff --git a/src/components/Message/hooks/useMuteHandler.ts b/src/components/Message/hooks/useMuteHandler.ts index a710a9cafc..e7fd9e4701 100644 --- a/src/components/Message/hooks/useMuteHandler.ts +++ b/src/components/Message/hooks/useMuteHandler.ts @@ -46,7 +46,7 @@ export const useMuteHandler = ( const successMessage = (getSuccessNotification && validateAndGetMessage(getSuccessNotification, [message.user])) || - t('{{ user }} has been muted', { + t('common.muted.label', '{{ user }} has been muted', { user: message.user.name || message.user.id, }); @@ -56,7 +56,7 @@ export const useMuteHandler = ( const errorMessage = (getErrorNotification && validateAndGetMessage(getErrorNotification, [message.user])) || - t('Error muting a user ...'); + t('common.errorMutingUser.label', 'Error muting a user ...'); if (typeof errorMessage === 'string') notify(errorMessage, 'error'); } @@ -68,7 +68,7 @@ export const useMuteHandler = ( const successMessage = (getSuccessNotification && validateAndGetMessage(getSuccessNotification, [message.user])) || - t('{{ user }} has been unmuted', { + t('common.unmuted.label', '{{ user }} has been unmuted', { user: message.user.name || message.user.id, }); @@ -78,7 +78,7 @@ export const useMuteHandler = ( const errorMessage = (getErrorNotification && validateAndGetMessage(getErrorNotification, [message.user])) || - t('Error unmuting a user ...'); + t('common.errorUnmutingUser.label', 'Error unmuting a user ...'); if (typeof errorMessage === 'string') notify(errorMessage, 'error'); } diff --git a/src/components/Message/hooks/usePinHandler.ts b/src/components/Message/hooks/usePinHandler.ts index 8caa1c659c..c5858e7833 100644 --- a/src/components/Message/hooks/usePinHandler.ts +++ b/src/components/Message/hooks/usePinHandler.ts @@ -70,7 +70,12 @@ export const usePinHandler = ( const errorMessage = getErrorNotification && validateAndGetMessage(getErrorNotification, [message]); - if (notify) notify(errorMessage || t('Error pinning message'), 'error'); + if (notify) + notify( + errorMessage || + t('common.errorPinningMessage.label', 'Error pinning message'), + 'error', + ); messagePaginator.ingestItem(message); } } else { @@ -90,7 +95,12 @@ export const usePinHandler = ( const errorMessage = getErrorNotification && validateAndGetMessage(getErrorNotification, [message]); - if (notify) notify(errorMessage || t('Error removing message pin'), 'error'); + if (notify) + notify( + errorMessage || + t('common.errorRemovingMessagePin.label', 'Error removing message pin'), + 'error', + ); messagePaginator.ingestItem(message); } } diff --git a/src/components/Message/utils.tsx b/src/components/Message/utils.tsx index 930439f4cb..7882ef1a15 100644 --- a/src/components/Message/utils.tsx +++ b/src/components/Message/utils.tsx @@ -342,25 +342,37 @@ export const getReadByTooltipText = ( } else if (slicedArr.length === 2) { // joins all with "and" but =no commas // example: "bob and sam" - outStr = t('{{ firstUser }} and {{ secondUser }}', { - firstUser: slicedArr[0], - secondUser: slicedArr[1], - }); + outStr = t( + 'message.and.withFirstUserAndSecondUser.label', + '{{ firstUser }} and {{ secondUser }}', + { + firstUser: slicedArr[0], + secondUser: slicedArr[1], + }, + ); } else if (slicedArr.length > 2) { // joins all with commas, but last one gets ", and" (oxford comma!) // example: "bob, joe, sam and 4 more" if (restLength === 0) { // mutate slicedArr to remove last user to display it separately const lastUser = slicedArr.splice(slicedArr.length - 1, 1); - outStr = t('{{ commaSeparatedUsers }}, and {{ lastUser }}', { - commaSeparatedUsers: slicedArr.join(', '), - lastUser, - }); + outStr = t( + 'message.and.withCommaSeparatedUsersAndLastUser.label', + '{{ commaSeparatedUsers }}, and {{ lastUser }}', + { + commaSeparatedUsers: slicedArr.join(', '), + lastUser, + }, + ); } else { - outStr = t('{{ commaSeparatedUsers }} and {{ moreCount }} more', { - commaSeparatedUsers: slicedArr.join(', '), - moreCount: restLength, - }); + outStr = t( + 'message.more.label', + '{{ commaSeparatedUsers }} and {{ moreCount }} more', + { + commaSeparatedUsers: slicedArr.join(', '), + moreCount: restLength, + }, + ); } } diff --git a/src/components/MessageActions/DeleteMessageAlert.tsx b/src/components/MessageActions/DeleteMessageAlert.tsx index 64786be772..342c5d6883 100644 --- a/src/components/MessageActions/DeleteMessageAlert.tsx +++ b/src/components/MessageActions/DeleteMessageAlert.tsx @@ -18,8 +18,14 @@ export const DeleteMessageAlert = ({ onCancel, onDelete }: DeleteMessageAlertPro data-testid='message-delete-alert' > diff --git a/src/components/MessageActions/DownloadSubmenu.tsx b/src/components/MessageActions/DownloadSubmenu.tsx index dfe3c4b430..aa69552e6f 100644 --- a/src/components/MessageActions/DownloadSubmenu.tsx +++ b/src/components/MessageActions/DownloadSubmenu.tsx @@ -24,7 +24,7 @@ export const DownloadSubmenuHeader = () => { - {t('Download Attachment')} + {t('common.downloadAttachment.title', 'Download Attachment')} ); @@ -44,8 +44,16 @@ export const DownloadSubmenu = () => { {downloadableAttachments.map((attachment, index) => { const fileName = attachment.localMetadata?.file?.name ?? attachment.title; const label = fileName - ? t('Download {{ fileName }}', { fileName }) - : t('Download attachment {{ number }}', { number: index + 1 }); + ? t( + 'messageActions.downloadSubmenu.download.label', + 'Download {{ fileName }}', + { fileName }, + ) + : t( + 'messageActions.downloadSubmenu.downloadAttachment.label', + 'Download attachment {{ number }}', + { number: index + 1 }, + ); return ( { closeMenu(); }} > - {t('Download All')} + {t('messageActions.downloadSubmenu.download.text', 'Download All')}
); diff --git a/src/components/MessageActions/MessageActions.defaults.tsx b/src/components/MessageActions/MessageActions.defaults.tsx index 1062ec759f..ec1bec935b 100644 --- a/src/components/MessageActions/MessageActions.defaults.tsx +++ b/src/components/MessageActions/MessageActions.defaults.tsx @@ -106,7 +106,10 @@ const DefaultMessageActionComponents = { - {t('Add reaction')} + {t('common.addReaction.text', 'Add reaction')} ); @@ -138,7 +141,7 @@ const DefaultMessageActionComponents = { return ( - {t('Thread Reply')} + {t('messageActions.threadReply.text', 'Thread Reply')} ); }, @@ -172,7 +175,7 @@ const DefaultMessageActionComponents = { return ( { @@ -180,7 +183,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {t('Quote Reply')} + {t('messageActions.quoteReply.text', 'Quote Reply')} ); }, @@ -197,7 +200,7 @@ const DefaultMessageActionComponents = { return ( 1} Icon={IconDownload} @@ -215,7 +218,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {t('Download Attachment')} + {t('common.downloadAttachment.title', 'Download Attachment')} ); }, @@ -227,7 +230,11 @@ const DefaultMessageActionComponents = { const isPinned = !!message.pinned; return ( { @@ -238,7 +245,9 @@ const DefaultMessageActionComponents = { message, }, emitter: 'MessageActions', - message: isPinned ? t('Message unpinned') : t('Message pinned'), + message: isPinned + ? t('messageActions.messageUnpinned.text', 'Message unpinned') + : t('common.messagePinned.label', 'Message pinned'), severity: 'success', type: isPinned ? 'api:message:unpin:success' : 'api:message:pin:success', }); @@ -251,7 +260,12 @@ const DefaultMessageActionComponents = { error: getNotificationError(error), message: getErrorMessage( error, - isPinned ? t('Error removing message pin') : t('Error pinning message'), + isPinned + ? t( + 'common.errorRemovingMessagePin.label', + 'Error removing message pin', + ) + : t('common.errorPinningMessage.label', 'Error pinning message'), ), severity: 'error', type: isPinned ? 'api:message:unpin:failed' : 'api:message:pin:failed', @@ -260,7 +274,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {isPinned ? t('Unpin') : t('Pin')} + {isPinned ? t('common.unpin.title', 'Unpin') : t('common.pin.title', 'Pin')} ); }, @@ -271,7 +285,7 @@ const DefaultMessageActionComponents = { return ( { @@ -279,7 +293,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {t('Copy Message')} + {t('messageActions.copyMessage.text', 'Copy Message')} ); }, @@ -290,7 +304,7 @@ const DefaultMessageActionComponents = { return ( { @@ -298,7 +312,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {t('Resend')} + {t('messageActions.resend.text', 'Resend')} ); }, @@ -310,7 +324,7 @@ const DefaultMessageActionComponents = { return ( { @@ -319,7 +333,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {t('Edit Message')} + {t('common.editMessage.text', 'Edit Message')} ); }, @@ -331,7 +345,10 @@ const DefaultMessageActionComponents = { return ( { @@ -342,7 +359,10 @@ const DefaultMessageActionComponents = { message, }, emitter: 'MessageActions', - message: t('Message marked as unread'), + message: t( + 'messageActions.messageMarkedUnread.text', + 'Message marked as unread', + ), severity: 'success', type: 'api:message:markUnread:success', }); @@ -356,6 +376,7 @@ const DefaultMessageActionComponents = { message: getErrorMessage( error, t( + 'messageActions.errorMarkingMessageUnread.text', 'Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.', ), ), @@ -366,7 +387,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {t('Mark as unread')} + {t('messageActions.markUnread.text', 'Mark as unread')} ); }, @@ -383,7 +404,11 @@ const DefaultMessageActionComponents = { return ( - {reminder ? t('Remove reminder') : t('Remind me')} + {reminder + ? t('messageActions.removeReminder.text', 'Remove reminder') + : t('messageActions.remindMe.text', 'Remind me')} ); }, @@ -441,7 +468,9 @@ const DefaultMessageActionComponents = { return ( - {reminder ? t('Remove save for later') : t('Save for later')} + {reminder + ? t('messageActions.removeSaveLater.text', 'Remove save for later') + : t('messageActions.saveLater.text', 'Save for later')} ); }, @@ -505,7 +539,7 @@ const DefaultMessageActionComponents = { return ( { @@ -516,7 +550,10 @@ const DefaultMessageActionComponents = { message, }, emitter: 'MessageActions', - message: t('Message has been successfully flagged'), + message: t( + 'messageActions.messageSuccessfullyFlagged.text', + 'Message has been successfully flagged', + ), severity: 'success', type: 'api:message:flag:success', }); @@ -527,7 +564,10 @@ const DefaultMessageActionComponents = { }, emitter: 'MessageActions', error: getNotificationError(error), - message: getErrorMessage(error, t('Error adding flag')), + message: getErrorMessage( + error, + t('messageActions.errorAddingFlag.text', 'Error adding flag'), + ), severity: 'error', type: 'api:message:flag:failed', }); @@ -535,7 +575,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {t('Flag')} + {t('messageActions.flag.text', 'Flag')} ); }, @@ -549,7 +589,11 @@ const DefaultMessageActionComponents = { const isMuted = isUserMuted(message, mutes); return ( { @@ -561,10 +605,10 @@ const DefaultMessageActionComponents = { }, emitter: 'MessageActions', message: isMuted - ? t('{{ user }} has been unmuted', { + ? t('common.unmuted.label', '{{ user }} has been unmuted', { user: message.user?.name || message.user?.id, }) - : t('{{ user }} has been muted', { + : t('common.muted.label', '{{ user }} has been muted', { user: message.user?.name || message.user?.id, }), severity: 'success', @@ -579,7 +623,9 @@ const DefaultMessageActionComponents = { error: getNotificationError(error), message: getErrorMessage( error, - isMuted ? t('Error unmuting a user ...') : t('Error muting a user ...'), + isMuted + ? t('common.errorUnmutingUser.label', 'Error unmuting a user ...') + : t('common.errorMutingUser.label', 'Error muting a user ...'), ), severity: 'error', type: isMuted ? 'api:user:unmute:failed' : 'api:user:mute:failed', @@ -588,7 +634,7 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {isMuted ? t('Unmute') : t('Mute')} + {isMuted ? t('common.unmute.title', 'Unmute') : t('common.mute.title', 'Mute')} ); }, @@ -605,7 +651,7 @@ const DefaultMessageActionComponents = { return ( <> { @@ -613,7 +659,7 @@ const DefaultMessageActionComponents = { }} variant='destructive' > - {t('Delete message')} + {t('messageActions.deleteMessageAlert.deleteMessage.title', 'Delete message')} { @@ -677,7 +730,9 @@ const DefaultMessageActionComponents = { closeMenu(); }} > - {isBlocked ? t('Unblock') : t('Block User')} + {isBlocked + ? t('common.unblock.ariaLabel', 'Unblock') + : t('common.blockUser.title', 'Block User')} ); }, @@ -705,7 +760,10 @@ const DefaultMessageActionComponents = { { @@ -729,7 +787,7 @@ const DefaultMessageActionComponents = { return ( { - {t('Remind Me')} + {t('messageActions.remindMeSubmenu.remindMe.text', 'Remind Me')} ); @@ -58,7 +58,7 @@ export const RemindMeSubmenu = () => { message, }, emitter: 'MessageActions', - message: t('Reminder set'), + message: t('common.reminderSet.text', 'Reminder set'), severity: 'success', type: 'api:message:reminder:set:success', }); @@ -78,7 +78,7 @@ export const RemindMeSubmenu = () => { } }} > - {t('duration/Remind Me', { milliseconds: offsetMs })} + {t('duration.remindMe', { milliseconds: offsetMs })} ))} {/* todo: potential improvement to add a custom option that would trigger rendering modal with custom date picker - we need date picker */} diff --git a/src/components/MessageBounce/MessageBouncePrompt.tsx b/src/components/MessageBounce/MessageBouncePrompt.tsx index d24bd5642a..528776cacd 100644 --- a/src/components/MessageBounce/MessageBouncePrompt.tsx +++ b/src/components/MessageBounce/MessageBouncePrompt.tsx @@ -37,13 +37,19 @@ export function MessageBouncePrompt({ children }: MessageBouncePromptProps) { description={ !children ? t( + 'messageBounce.prompt.description', 'Review this message and choose whether to delete it, edit it, or send it anyway', ) : undefined } Icon={IconExclamationMark} title={ - !children ? t('This message did not meet our content guidelines') : undefined + !children + ? t( + 'messageBounce.prompt.title', + 'This message did not meet our content guidelines', + ) + : undefined } > {children} @@ -57,7 +63,7 @@ export function MessageBouncePrompt({ children }: MessageBouncePromptProps) { size='md' variant='danger' > - {t('Delete')} + {t('common.delete.text', 'Delete')} diff --git a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx index 4f878f9733..87b766672a 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.tsx @@ -92,7 +92,9 @@ export const AudioAttachmentPreview = ({
- {isVoiceRecordingAttachment(attachment) ? t('Voice message') : attachment.title} + {isVoiceRecordingAttachment(attachment) + ? t('common.voiceMessage.label', 'Voice message') + : attachment.title}
{uploadState === 'uploading' && ( @@ -134,18 +136,32 @@ export const AudioAttachmentPreview = ({ {hasSizeLimitError - ? t('File too large') + ? t( + 'messageComposer.audioAttachmentPreview.fileTooLarge.text', + 'File too large', + ) : uploadState === 'blocked' - ? t('Upload blocked') - : t('Upload failed')} + ? t( + 'messageComposer.audioAttachmentPreview.uploadBlocked.text', + 'Upload blocked', + ) + : t( + 'messageComposer.audioAttachmentPreview.uploadFailed.text', + 'Upload failed', + )}
) : (
- {t('Upload error')} + + {t( + 'messageComposer.audioAttachmentPreview.uploadError.text', + 'Upload error', + )} +
)} @@ -161,7 +180,7 @@ export const AudioAttachmentPreview = ({
{audioPlayer && canPlayRecord && ( {hasSizeLimitError - ? t('File too large') + ? t( + 'messageComposer.audioAttachmentPreview.fileTooLarge.text', + 'File too large', + ) : uploadState === 'blocked' - ? t('Upload blocked') - : t('Upload failed')} + ? t( + 'messageComposer.audioAttachmentPreview.uploadBlocked.text', + 'Upload blocked', + ) + : t( + 'messageComposer.audioAttachmentPreview.uploadFailed.text', + 'Upload failed', + )} )} {hasRetriableError && (
- {t('Upload error')} + + {t( + 'messageComposer.audioAttachmentPreview.uploadError.text', + 'Upload error', + )} +
)} diff --git a/src/components/MessageComposer/AttachmentPreviewList/GeolocationPreview.tsx b/src/components/MessageComposer/AttachmentPreviewList/GeolocationPreview.tsx index b6a3417789..1c9d996a06 100644 --- a/src/components/MessageComposer/AttachmentPreviewList/GeolocationPreview.tsx +++ b/src/components/MessageComposer/AttachmentPreviewList/GeolocationPreview.tsx @@ -28,7 +28,9 @@ export const GeolocationPreview = ({ }: GeolocationPreviewProps) => { const { t } = useTranslationContext(); const shareDuration = (location as LiveLocationPreview).durationMs; - const title = shareDuration ? t('Live location') : t('Current location'); + const title = shareDuration + ? t('common.liveLocation.text', 'Live location') + : t('common.currentLocation.text', 'Current location'); return (
@@ -36,19 +38,26 @@ export const GeolocationPreview = ({
{title}
- {t('Location: {{ coordinates }}', { - coordinates: `${location.latitude}, ${location.longitude}`, - })} + {t( + 'messageComposer.geolocationPreview.location.text', + 'Location: {{ coordinates }}', + { + coordinates: `${location.latitude}, ${location.longitude}`, + }, + )}
{shareDuration && (
- {t('Live for {{duration}}', { - duration: t('duration/Share Location', { + {t('messageComposer.geolocationPreview.live.text', 'Live for {{duration}}', { + duration: t('duration.shareLocation', { milliseconds: shareDuration, }), })} @@ -57,7 +66,10 @@ export const GeolocationPreview = ({
{remove && ( - {t('Unsupported attachment')} + {t('common.unsupportedAttachment.text', 'Unsupported attachment')}
inputRef.current?.click()} ref={setButtonElement} @@ -189,7 +192,7 @@ export const DefaultAttachmentSelectorComponents = { }); }} > - {t('Commands')} + {t('messageComposer.attachmentSelector.commands.text', 'Commands')} ); }, @@ -207,7 +210,7 @@ export const DefaultAttachmentSelectorComponents = { closeMenu(); }} > - {t('File')} + {t('messageComposer.attachmentSelector.file.text', 'File')} ); }, @@ -223,7 +226,7 @@ export const DefaultAttachmentSelectorComponents = { closeMenu(); }} > - {t('Location')} + {t('common.location.text', 'Location')} ); }, @@ -239,7 +242,7 @@ export const DefaultAttachmentSelectorComponents = { closeMenu(); }} > - {t('Poll')} + {t('common.poll.label', 'Poll')} ); }, @@ -439,7 +442,10 @@ export const AttachmentSelector = ({ {...buttonProps} aria-expanded={menuDialogIsOpen} aria-haspopup='true' - aria-label={t('aria/Open Attachment Selector')} + aria-label={t( + 'messageComposer.attachmentSelector.openAttachmentSelector.ariaLabel', + 'Open Attachment Selector', + )} disabled={isCooldownActive} iconClassName={clsx('str-chat__prepare-rotate45', { 'str-chat__rotate45': menuDialogIsOpen, @@ -449,8 +455,11 @@ export const AttachmentSelector = ({ /> { return ( - {t('Instant commands')} + + {t('messageComposer.commandsMenu.instantCommands.text', 'Instant commands')} + ); @@ -51,7 +56,9 @@ export const CommandsMenuHeader = () => { const { t } = useTranslationContext(); return ( - {t('Instant commands')} + + {t('messageComposer.commandsMenu.instantCommands.text', 'Instant commands')} + ); }; @@ -91,21 +98,21 @@ export const useCommandTranslation = (command: Command) => { const knownArgsTranslations = useMemo>( () => ({ - ban: t('ban-command-args'), - giphy: t('giphy-command-args'), - mute: t('mute-command-args'), - unban: t('unban-command-args'), - unmute: t('unmute-command-args'), + ban: t('command.ban.args', '[@username] [text]'), + giphy: t('command.giphy.args', '[text]'), + mute: t('command.mute.args', '[@username]'), + unban: t('command.unban.args', '[@username]'), + unmute: t('command.unmute.args', '[@username]'), }), [t], ); const knownDescriptionTranslations = useMemo>( () => ({ - ban: t('ban-command-description'), - giphy: t('giphy-command-description'), - mute: t('mute-command-description'), - unban: t('unban-command-description'), - unmute: t('unmute-command-description'), + ban: t('command.ban.description', 'Ban a user'), + giphy: t('command.giphy.description', 'Post a random gif to the channel'), + mute: t('command.mute.description', 'Mute a user'), + unban: t('command.unban.description', 'Unban a user'), + unmute: t('command.unmute.description', 'Unmute a user'), }), [t], ); diff --git a/src/components/MessageComposer/AttachmentSelector/__tests__/CommandsMenu.test.tsx b/src/components/MessageComposer/AttachmentSelector/__tests__/CommandsMenu.test.tsx index 43ecb142cd..bb76d5db37 100644 --- a/src/components/MessageComposer/AttachmentSelector/__tests__/CommandsMenu.test.tsx +++ b/src/components/MessageComposer/AttachmentSelector/__tests__/CommandsMenu.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { CommandsMenu, CommandsSubmenuHeader } from '../CommandsMenu'; +import { mockT } from '../../../../mock-builders/translator'; const { announceInteraction, closeMenu, returnToParentMenu, setCommand } = vi.hoisted( () => ({ @@ -14,8 +15,7 @@ const { announceInteraction, closeMenu, returnToParentMenu, setCommand } = vi.ho const { commandsMock } = vi.hoisted(() => ({ commandsMock: { value: [] as unknown[] } })); -// Strip the `aria/` prefix so assertions read the natural-language value. -const t = (key: string) => (key.startsWith('aria/') ? key.replace('aria/', '') : key); +const t = mockT; // Keep the real Dialog primitives (ContextMenuBackButton/Button/Header) — only stub the context hook. vi.mock('../../../Dialog', async (importOriginal) => ({ diff --git a/src/components/MessageComposer/CommandChip.tsx b/src/components/MessageComposer/CommandChip.tsx index 436a0b4eb4..33d4f6d17d 100644 --- a/src/components/MessageComposer/CommandChip.tsx +++ b/src/components/MessageComposer/CommandChip.tsx @@ -20,7 +20,11 @@ export const CommandChip = ({ command }: CommandChipProps) => { {command.name} diff --git a/src/components/Poll/PollActions/PollActions.tsx b/src/components/Poll/PollActions/PollActions.tsx index 59524354b3..53085942b3 100644 --- a/src/components/Poll/PollActions/PollActions.tsx +++ b/src/components/Poll/PollActions/PollActions.tsx @@ -92,7 +92,7 @@ export const PollActions = ({
{!is_closed && created_by_id === client.user?.id && ( 0 && channelCapabilities.has('query-poll-votes') && (
@@ -64,7 +67,10 @@ export const PollAnswerList = ({ onUpdateOwnAnswerClick }: PollAnswerListProps) size='md' variant='secondary' > - {t('Update your comment')} + {t( + 'poll.addCommentPrompt.updateComment.label', + 'Update Your Comment', + )}
)} diff --git a/src/components/Poll/PollActions/PollOptionsFullList.tsx b/src/components/Poll/PollActions/PollOptionsFullList.tsx index aa6d58e80e..3c73f24366 100644 --- a/src/components/Poll/PollActions/PollOptionsFullList.tsx +++ b/src/components/Poll/PollActions/PollOptionsFullList.tsx @@ -22,8 +22,11 @@ export const PollOptionsFullList = () => { diff --git a/src/components/Poll/PollActions/PollQuestion.tsx b/src/components/Poll/PollActions/PollQuestion.tsx index 7f08ffd480..2d59692626 100644 --- a/src/components/Poll/PollActions/PollQuestion.tsx +++ b/src/components/Poll/PollActions/PollQuestion.tsx @@ -8,7 +8,9 @@ export const PollQuestion = ({ question }: PollQuestionProps) => { const { t } = useTranslationContext(); return (
-
{t('Question')}
+
+ {t('poll.question.question.text', 'Question')} +
{question}
); diff --git a/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx b/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx index 5a89706e2c..d9719b7150 100644 --- a/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx +++ b/src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx @@ -65,7 +65,7 @@ export const PollOptionWithVotes = ({ size='md' variant='secondary' > - {t('View all')} + {t('poll.optionVotes.view.text', 'View all')}
)} diff --git a/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx b/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx index 3aaf128d91..af4d42b26d 100644 --- a/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx +++ b/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx @@ -33,7 +33,11 @@ export const PollResultOptionVoteCounter = ({ )} - {t('{{count}} votes', { count: vote_counts_by_option[optionId] ?? 0 })} + {t('poll.optionVotes.votes.text', { + count: vote_counts_by_option[optionId] ?? 0, + defaultValue_one: '{{count}} vote', + defaultValue_other: '{{count}} votes', + })} ); @@ -53,7 +57,9 @@ export const PollOptionWithVotesHeader = ({ return (
- {t('Question {{ optionOrderNumber}}', { optionOrderNumber })} + {t('poll.optionVotes.question.text', 'Question {{ optionOrderNumber}}', { + optionOrderNumber, + })}
{option.text}
diff --git a/src/components/Poll/PollActions/PollResults/PollResults.tsx b/src/components/Poll/PollActions/PollResults/PollResults.tsx index dbf45440a3..eb1b0392be 100644 --- a/src/components/Poll/PollActions/PollResults/PollResults.tsx +++ b/src/components/Poll/PollActions/PollResults/PollResults.tsx @@ -50,9 +50,12 @@ export const PollResults = () => { <> { @@ -98,7 +102,11 @@ export const PollResults = () => {
- {t('totalVoteCount', { count: vote_count })} + {t('poll.results.totalVoteCount.text', { + count: vote_count, + defaultValue_one: '1 vote total', + defaultValue_other: '{{ count }} votes total', + })}
diff --git a/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx b/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx index 4dbb26fae7..3a6cc1453c 100644 --- a/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx +++ b/src/components/Poll/PollActions/SuggestPollOptionPrompt.tsx @@ -32,11 +32,21 @@ export const SuggestPollOptionPrompt = () => { optionText: (v: string) => { const trimmed = typeof v === 'string' ? v.trim() : ''; if (!trimmed) { - return new Error(t('This field cannot be empty or contain only spaces')); + return new Error( + t( + 'poll.addCommentPrompt.fieldCannotEmptyContain.label', + 'This field cannot be empty or contain only spaces', + ), + ); } const existingOption = options.find((option) => option.text === trimmed); if (existingOption) { - return new Error(t('Option already exists')); + return new Error( + t( + 'poll.suggestPollOption.optionAlreadyExists.label', + 'Option already exists', + ), + ); } return undefined; }, @@ -73,19 +83,22 @@ export const SuggestPollOptionPrompt = () => {
setFieldValue('optionText', e.target.value)} - placeholder={t('placeholder/PollOptionSuggestion')} + placeholder={t('poll.pollOptionSuggestion.placeholder', 'Enter a new option')} ref={setInput} required type='text' @@ -98,14 +111,14 @@ export const SuggestPollOptionPrompt = () => { className='str-chat__prompt__footer__controls-button--cancel' onClick={close} > - {t('Cancel')} + {t('common.cancel.label', 'Cancel')} 0 || submitDisabled} type='submit' > - {t('Send')} + {t('common.send.label', 'Send')} diff --git a/src/components/Poll/PollActions/__tests__/EndPollAlert.test.tsx b/src/components/Poll/PollActions/__tests__/EndPollAlert.test.tsx index d6aa9366fb..ffcd244bb7 100644 --- a/src/components/Poll/PollActions/__tests__/EndPollAlert.test.tsx +++ b/src/components/Poll/PollActions/__tests__/EndPollAlert.test.tsx @@ -16,6 +16,7 @@ import { mockTranslationContextValue, } from '../../../../mock-builders'; import { Poll } from 'stream-chat'; +import { mockT } from '../../../../mock-builders/translator'; describe('EndPollAlert', () => { it('closes modal and notifies on successful poll end', async () => { @@ -29,7 +30,7 @@ describe('EndPollAlert', () => { render( - k })}> + @@ -48,7 +49,7 @@ describe('EndPollAlert', () => { expect(close).toHaveBeenCalledTimes(1); expect(addSpy).toHaveBeenCalledWith( expect.objectContaining({ - message: 'Poll ended', + message: 'Poll Ended', options: expect.objectContaining({ severity: 'success' }), }), ); @@ -66,7 +67,7 @@ describe('EndPollAlert', () => { render( - k })}> + diff --git a/src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx b/src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx index 53be789d2a..d24e30d1c3 100644 --- a/src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx +++ b/src/components/Poll/PollCreationDialog/MultipleAnswersField.tsx @@ -24,8 +24,14 @@ export const MultipleAnswersField = () => { const knownValidationErrors = useMemo>( () => ({ - 'Enforce unique vote is enabled': t('Enforce unique vote is enabled'), - 'Type a number from 2 to 10': t('Type a number from 2 to 10'), + 'Enforce unique vote is enabled': t( + 'poll.multipleAnswersField.enforceUniqueVoteEnabled.label', + 'Enforce unique vote is enabled', + ), + 'Type a number from 2 to 10': t( + 'poll.multipleAnswersField.typeNumber210.label', + 'Type a number from 2 to 10', + ), }), [t], ); @@ -39,13 +45,16 @@ export const MultipleAnswersField = () => {
{ setVoteLimitEnabled(false); pollComposer.updateFields({ enforce_unique_vote: !e.target.checked }); }} - title={t('Multiple votes')} + title={t('poll.multipleAnswersField.multipleVotes.title', 'Multiple Votes')} /> {multipleVotesEnabled && ( {
{voteLimitEnabled && ( { const raw = e.target.value; const nativeFieldValidation = raw !== '' && !/^\d+$/.test(raw) - ? { max_votes_allowed: t('Only numbers are allowed') } + ? { + max_votes_allowed: t( + 'poll.multipleAnswersField.onlyNumbersAllowed.label', + 'Only numbers are allowed', + ), + } : undefined; pollComposer.updateFields( { diff --git a/src/components/Poll/PollCreationDialog/NameField.tsx b/src/components/Poll/PollCreationDialog/NameField.tsx index e100fdad82..4abc88aa08 100644 --- a/src/components/Poll/PollCreationDialog/NameField.tsx +++ b/src/components/Poll/PollCreationDialog/NameField.tsx @@ -16,7 +16,10 @@ export const NameField = () => { const { error, name } = useStateStore(pollComposer.state, pollComposerStateSelector); const knownValidationErrors = useMemo>( () => ({ - 'Question is required': t('Question is required'), + 'Question is required': t( + 'poll.nameField.questionRequired.label', + 'Question is required', + ), }), [t], ); @@ -32,19 +35,19 @@ export const NameField = () => { errorMessage={ error ? ( - {knownValidationErrors[error] ?? t('Error')} + {knownValidationErrors[error] ?? t('poll.nameField.error.text', 'Error')} ) : undefined } id='name' - label={t('Question')} + label={t('poll.question.question.text', 'Question')} onBlur={() => { pollComposer.handleFieldBlur('name'); }} onChange={(e) => { pollComposer.updateFields({ name: e.target.value }); }} - placeholder={t('Ask a question')} + placeholder={t('poll.nameField.askQuestion.placeholder', 'Ask a Question')} type='text' value={name} /> diff --git a/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx b/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx index e597ce70a1..7efee453d5 100644 --- a/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx +++ b/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx @@ -45,15 +45,19 @@ export const OptionFieldSet = () => { const knownValidationErrors = useMemo>( () => ({ - 'Option already exists': t('Option already exists'), - 'Option is empty': t('Option is empty'), + 'Option already exists': t( + 'poll.suggestPollOption.optionAlreadyExists.label', + 'Option already exists', + ), + 'Option is empty': t('poll.optionFieldSet.optionEmpty.label', 'Option is empty'), }), [t], ); const labelForOption = useCallback( (option: PollComposerOption, position: number) => - option.text.trim() || t('aria/Option {{ position }}', { position }), + option.text.trim() || + t('poll.optionFieldSet.option.ariaLabel', 'Option {{ position }}', { position }), [t], ); @@ -188,7 +192,10 @@ export const OptionFieldSet = () => { useSettledAnnouncement(announce, { active: draggable, - message: t('aria/Options can now be reordered and removed.'), + message: t( + 'poll.optionFieldSet.optionsCanNowReordered.ariaLabel', + 'Options can now be reordered and removed.', + ), settleKey: options, }); @@ -196,7 +203,7 @@ export const OptionFieldSet = () => { <> {options.map((option, i) => { @@ -232,7 +239,8 @@ export const OptionFieldSet = () => { message={ error ? ( - {knownValidationErrors[error] ?? t('Error')} + {knownValidationErrors[error] ?? + t('poll.nameField.error.text', 'Error')} ) : undefined } @@ -250,7 +258,10 @@ export const OptionFieldSet = () => { optionInputRefs.current[i + 1]?.focus(); } }} - placeholder={t('Add an option')} + placeholder={t( + 'poll.optionFieldSet.addOption.placeholder', + 'Add an Option', + )} ref={(element) => { optionInputRefs.current[i] = element; }} @@ -258,9 +269,13 @@ export const OptionFieldSet = () => { draggable ? ( clearOption(option.id)} /> ) : undefined @@ -274,7 +289,10 @@ export const OptionFieldSet = () => { {draggable && ( - {t('aria/This option can be reordered and removed.')} + {t( + 'poll.optionFieldSet.optionCanReorderedRemoved.ariaLabel', + 'This option can be reordered and removed.', + )} )} diff --git a/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx b/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx index 57a890f289..65dd51adcb 100644 --- a/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx +++ b/src/components/Poll/PollCreationDialog/PollCreationDialog.tsx @@ -40,8 +40,11 @@ export const PollCreationDialog = ({ close }: PollCreationDialogProps) => { > @@ -51,7 +54,10 @@ export const PollCreationDialog = ({ close }: PollCreationDialogProps) => { pollComposer.updateFields({ @@ -60,27 +66,33 @@ export const PollCreationDialog = ({ close }: PollCreationDialogProps) => { : VotingVisibility.public, }) } - title={t('Anonymous poll')} + title={t('poll.creationDialog.anonymousPoll.title', 'Anonymous Poll')} /> pollComposer.updateFields({ allow_user_suggested_options: e.target.checked, }) } - title={t('Suggest an option')} + title={t('poll.actions.suggestOption.label', 'Suggest an Option')} /> pollComposer.updateFields({ allow_answers: e.target.checked }) } - title={t('Add a comment')} + title={t('poll.addCommentPrompt.addComment.label', 'Add a Comment')} />
diff --git a/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx b/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx index 8f2f15531d..da41970901 100644 --- a/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx +++ b/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx @@ -31,7 +31,7 @@ export const PollCreationDialogControls = ({ onClick={close} type='button' > - {t('Cancel')} + {t('common.cancel.label', 'Cancel')} - {t('Send poll')} + {t('poll.creationDialog.sendPoll.text', 'Send Poll')} diff --git a/src/components/Poll/PollCreationDialog/PollOptionReorderHandle.tsx b/src/components/Poll/PollCreationDialog/PollOptionReorderHandle.tsx index edcc0a14d7..f2fdecaaaa 100644 --- a/src/components/Poll/PollCreationDialog/PollOptionReorderHandle.tsx +++ b/src/components/Poll/PollCreationDialog/PollOptionReorderHandle.tsx @@ -36,7 +36,9 @@ export const PollOptionReorderHandle = ({ const focusAnnouncementFrameRef = useRef(null); const position = index + 1; - const optionLabel = option.text.trim() || t('aria/Option {{ position }}', { position }); + const optionLabel = + option.text.trim() || + t('poll.optionFieldSet.option.ariaLabel', 'Option {{ position }}', { position }); // While picked up, fold the option text + new position into the aria-label // so VoiceOver speaks "Reorder 'option B' at position 1 of 3" on the focus // event triggered by ArrowUp/ArrowDown. That replaces the otherwise @@ -44,12 +46,18 @@ export const PollOptionReorderHandle = ({ // and removes the need for a duplicate live-region "moved to position" // message. const ariaLabel = isActive - ? t('aria/Reorder "{{ option }}" at position {{ position }} of {{ total }}', { - option: optionLabel, + ? t( + 'poll.optionReorder.reorderPosition.ariaLabel', + 'Reorder "{{ option }}" at position {{ position }} of {{ total }}', + { + option: optionLabel, + position, + total: totalOptionCount, + }, + ) + : t('poll.optionReorder.reorderOption.ariaLabel', 'Reorder option {{ position }}', { position, - total: totalOptionCount, - }) - : t('aria/Reorder option {{ position }}', { position }); + }); useEffect( () => () => { @@ -108,7 +116,8 @@ export const PollOptionReorderHandle = ({ focusAnnouncementFrameRef.current = requestAnimationFrame(() => { announce( t( - 'aria/Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.', + 'poll.optionReorder.pressSpaceSelectOption.ariaLabel', + 'Press Space to select this option, use the Up and Down arrow keys to move it, then press Space again to deselect it.', ), { priority: 'assertive' }, ); diff --git a/src/components/Poll/PollHeader.tsx b/src/components/Poll/PollHeader.tsx index d692dd680a..53b7553af5 100644 --- a/src/components/Poll/PollHeader.tsx +++ b/src/components/Poll/PollHeader.tsx @@ -26,13 +26,17 @@ export const PollHeader = () => { useStateStore(poll.state, pollStateSelector); const selectionInstructions = useMemo(() => { - if (is_closed) return t('Vote ended'); - if (enforce_unique_vote || options.length === 1) return t('Select one'); + if (is_closed) return t('poll.header.voteEnded.label', 'Vote ended'); + if (enforce_unique_vote || options.length === 1) + return t('poll.header.selectOne.label', 'Select one'); if (max_votes_allowed) - return t('Select up to {{count}}', { + return t('poll.header.selectUp.label', { count: max_votes_allowed > options.length ? options.length : max_votes_allowed, + defaultValue_one: 'Select up to {{count}}', + defaultValue_other: 'Select up to {{count}}', }); - if (options.length > 1) return t('Select one or more'); + if (options.length > 1) + return t('poll.header.selectOneMore.label', 'Select one or more'); return ''; }, [is_closed, enforce_unique_vote, max_votes_allowed, options.length, t]); diff --git a/src/components/Poll/PollOptionList.tsx b/src/components/Poll/PollOptionList.tsx index 502750146d..34bd312e94 100644 --- a/src/components/Poll/PollOptionList.tsx +++ b/src/components/Poll/PollOptionList.tsx @@ -54,8 +54,10 @@ export const PollOptionList = ({
{showMoreOptionsButton && ( {voteCountVerbose - ? t('{{count}} votes', { + ? t('poll.optionVotes.votes.text', { count: vote_counts_by_option[option.id] ?? 0, + defaultValue_one: '{{count}} vote', + defaultValue_other: '{{count}} votes', }) : (vote_counts_by_option[option.id] ?? 0)} diff --git a/src/components/Poll/PollVote.tsx b/src/components/Poll/PollVote.tsx index 3c9d00d4a9..0a0afea686 100644 --- a/src/components/Poll/PollVote.tsx +++ b/src/components/Poll/PollVote.tsx @@ -24,14 +24,14 @@ const PollVoteTimestamp = ({ timestamp }: { timestamp: string | Date }) => { onMouseLeave={handleLeave} ref={setReferenceElement} > - {t('timestamp/PollVote', { timestamp: timestampDate })} + {t('timestamp.PollVote', { timestamp: timestampDate })} - {t('timestamp/PollVoteTooltip', { timestamp: timestampDate })} + {t('timestamp.PollVoteTooltip', { timestamp: timestampDate })} ); @@ -50,8 +50,8 @@ const PollVoteAuthor = ({ vote }: PollVoteAuthor) => { useComponentContext(); const displayName = client.user?.id && client.user.id === vote.user?.id - ? t('You') - : vote.user?.name || vote.user?.id || t('Anonymous'); + ? t('common.you.label', 'You') + : vote.user?.name || vote.user?.id || t('common.anonymous.label', 'Anonymous'); return (
diff --git a/src/components/Poll/__tests__/AddCommentForm.test.tsx b/src/components/Poll/__tests__/AddCommentForm.test.tsx index 2d429493a6..10e89744ca 100644 --- a/src/components/Poll/__tests__/AddCommentForm.test.tsx +++ b/src/components/Poll/__tests__/AddCommentForm.test.tsx @@ -7,12 +7,13 @@ import { AddCommentPrompt } from '../PollActions'; import { PollProvider, TranslationProvider } from '../../../context'; import type { TranslationContextValue } from '../../../context'; import { generatePoll, mockTranslationContextValue } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; const close = vi.fn(); const messageId = 'messageId'; const newlyTypedValue = 'XX'; -const t = ((v: string) => v) as TranslationContextValue['t']; +const t = mockT as TranslationContextValue['t']; const renderComponent = ({ poll, props }: any = {}) => render( diff --git a/src/components/Poll/__tests__/Poll.test.tsx b/src/components/Poll/__tests__/Poll.test.tsx index 4bc1eb5bec..3fa9a6d020 100644 --- a/src/components/Poll/__tests__/Poll.test.tsx +++ b/src/components/Poll/__tests__/Poll.test.tsx @@ -19,12 +19,13 @@ import { mockChatContext, mockTranslationContextValue, } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; const POLL_ACTIONS__CLASS = '.str-chat__poll-actions'; const POLL_OPTION_LIST__CLASS = '.str-chat__poll-option-list'; const POLL_HEADER__CLASS = '.str-chat__poll-header'; -const t = (v) => v; +const t = mockT; // MERGE-RECONCILE (test migration): the deleted ChannelStateContext no longer provides // `channelCapabilities`. Poll components now read capabilities via useChannelCapabilities({ cid }), diff --git a/src/components/Poll/__tests__/PollActions.test.tsx b/src/components/Poll/__tests__/PollActions.test.tsx index 7a31d97952..f584b08cc6 100644 --- a/src/components/Poll/__tests__/PollActions.test.tsx +++ b/src/components/Poll/__tests__/PollActions.test.tsx @@ -21,6 +21,7 @@ import { mockMessageContext, mockTranslationContextValue, } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; // MERGE-RECONCILE (test migration): the deleted ChannelStateContext no longer provides // `channelCapabilities`. Poll components now read capabilities via useChannelCapabilities({ cid }), @@ -41,13 +42,13 @@ const makeChannel = (capabilities: Record = {}) => }, }); -const SUGGEST_OPTION_ACTION_TEXT = 'Suggest an option'; -const UPDATE_COMMENT_ACTION_TEXT = 'Update your comment'; -const VIEW_COMMENTS_ACTION_TEXT = 'View {{count}} comments'; -const VIEW_RESULTS_ACTION_TEXT = 'View results'; -const END_VOTE_ACTION_TEXT = 'End poll'; +const SUGGEST_OPTION_ACTION_TEXT = 'Suggest an Option'; +const UPDATE_COMMENT_ACTION_TEXT = 'Update Your Comment'; +const VIEW_COMMENTS_ACTION_TEXT = 'View 1 Comment'; +const VIEW_RESULTS_ACTION_TEXT = 'View Results'; +const END_VOTE_ACTION_TEXT = 'End Poll'; -const t = (v: any) => v; +const t = mockT; const defaultChannelStateContext = { channelCapabilities: { 'cast-poll-vote': true, 'query-poll-votes': true }, diff --git a/src/components/Poll/__tests__/PollHeader.test.tsx b/src/components/Poll/__tests__/PollHeader.test.tsx index 0f67aeba40..5b4189ab85 100644 --- a/src/components/Poll/__tests__/PollHeader.test.tsx +++ b/src/components/Poll/__tests__/PollHeader.test.tsx @@ -7,11 +7,12 @@ import { Poll } from 'stream-chat'; import type { StreamChat } from 'stream-chat'; import { fromPartial } from '@total-typescript/shoehorn'; import { generatePoll, mockTranslationContextValue } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; const TITLE_SELECTOR = '.str-chat__poll-title'; const SUBTITLE_SELECTOR = '.str-chat__poll-subtitle'; -const t = ((v: string) => v) as TranslationContextValue['t']; +const t = mockT as TranslationContextValue['t']; const renderComponent = ({ poll }) => render( @@ -61,7 +62,7 @@ describe('PollHeader', () => { const nameDiv = container.querySelector(TITLE_SELECTOR); const subtitleDiv = container.querySelector(SUBTITLE_SELECTOR); expect(nameDiv).toHaveTextContent(pollData.name); - expect(subtitleDiv).toHaveTextContent('Select up to {{count}}'); + expect(subtitleDiv).toHaveTextContent('Select up to 2'); }); it('should render Select one or more header', () => { diff --git a/src/components/Poll/__tests__/PollOptionList.test.tsx b/src/components/Poll/__tests__/PollOptionList.test.tsx index 88bb3e6936..300ad4739a 100644 --- a/src/components/Poll/__tests__/PollOptionList.test.tsx +++ b/src/components/Poll/__tests__/PollOptionList.test.tsx @@ -24,6 +24,7 @@ import { mockMessageContext, mockTranslationContextValue, } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; // MERGE-RECONCILE (test migration): the deleted ChannelStateContext no longer provides // `channelCapabilities`. Poll components now read capabilities via useChannelCapabilities({ cid }), @@ -50,7 +51,10 @@ const CHECKMARK_SELECTOR = '.str-chat__checkmark'; const CHECKMARK_CHECKED_SELECTOR = '.str-chat__checkmark--checked'; const VOTE_COUNT_SELECTOR = '.str-chat__poll-option-vote-count'; -const MORE_OPTIONS_ACTION_TEXT = '+{{count}} more options'; +// NOTE: the component interpolates `options.length` (6 here), not the number of *hidden* +// options (3). That reads oddly but is pre-existing behaviour — the previous assertion +// matched the uninterpolated template, so it never surfaced. +const MORE_OPTIONS_ACTION_TEXT = '+6 more options'; const pollWithNoVotes = generatePoll({ answers_count: 1, @@ -61,7 +65,7 @@ const pollWithNoVotes = generatePoll({ vote_counts_by_option: {}, }); -const t = (v: any) => v; +const t = mockT; const defaultChannelStateContext = { channelCapabilities: { 'cast-poll-vote': true }, diff --git a/src/components/Poll/__tests__/SuggestPollOptionForm.test.tsx b/src/components/Poll/__tests__/SuggestPollOptionForm.test.tsx index bafc7e1d20..40fbd6b335 100644 --- a/src/components/Poll/__tests__/SuggestPollOptionForm.test.tsx +++ b/src/components/Poll/__tests__/SuggestPollOptionForm.test.tsx @@ -11,12 +11,13 @@ import { mockChatContext, mockTranslationContextValue, } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; const SUBMIT_BUTTON_TEXT = 'Send'; const newlyTypedValue = 'XX'; -const t = ((v: string) => v) as TranslationContextValue['t']; +const t = mockT as TranslationContextValue['t']; const renderComponent = ({ client, poll, props }: any) => render( diff --git a/src/components/ReactFileUtilities/UploadButton.tsx b/src/components/ReactFileUtilities/UploadButton.tsx index 863613f75d..5e0cef11fa 100644 --- a/src/components/ReactFileUtilities/UploadButton.tsx +++ b/src/components/ReactFileUtilities/UploadButton.tsx @@ -59,7 +59,7 @@ export const UploadFileInput = forwardRef(function UploadFileInput( return ( 1} diff --git a/src/components/Reactions/MessageReactions.tsx b/src/components/Reactions/MessageReactions.tsx index c9078ae8bb..d572f569a6 100644 --- a/src/components/Reactions/MessageReactions.tsx +++ b/src/components/Reactions/MessageReactions.tsx @@ -148,7 +148,10 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => { return ( <>
{ > { key={reactionType} > handleReactionButtonClick(reactionType)} @@ -208,7 +218,10 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => { visualStyle === 'segmented' && (
  • )}
  • diff --git a/src/components/Reactions/ReactionSelector.tsx b/src/components/Reactions/ReactionSelector.tsx index 2cda7962f7..f03f7b44a1 100644 --- a/src/components/Reactions/ReactionSelector.tsx +++ b/src/components/Reactions/ReactionSelector.tsx @@ -90,7 +90,7 @@ export const ReactionSelector: ReactionSelectorInterface = (props) => { return (
    { ({ Component, name: reactionName, type: reactionType }) => (
  • )}
  • diff --git a/src/components/Search/SearchResults/SearchResultItem.tsx b/src/components/Search/SearchResults/SearchResultItem.tsx index 8ff807c719..e7a832d7b0 100644 --- a/src/components/Search/SearchResults/SearchResultItem.tsx +++ b/src/components/Search/SearchResults/SearchResultItem.tsx @@ -172,9 +172,13 @@ export const UserSearchResultItem = ({ item, onSelect }: UserSearchResultItemPro return (
    diff --git a/src/components/Search/SearchResults/SearchSourceResultsEmpty.tsx b/src/components/Search/SearchResults/SearchSourceResultsEmpty.tsx index abaae805bb..7fb0f41646 100644 --- a/src/components/Search/SearchResults/SearchSourceResultsEmpty.tsx +++ b/src/components/Search/SearchResults/SearchSourceResultsEmpty.tsx @@ -4,6 +4,8 @@ import { useTranslationContext } from '../../../context'; export const SearchSourceResultsEmpty = () => { const { t } = useTranslationContext(); return ( -
    {t('No results found')}
    +
    + {t('search.sourceResults.noResultsFound.text', 'No results found')} +
    ); }; diff --git a/src/components/Search/SearchResults/SearchSourceResultsLoadingIndicator.tsx b/src/components/Search/SearchResults/SearchSourceResultsLoadingIndicator.tsx index 6ddef10189..cbdff954b5 100644 --- a/src/components/Search/SearchResults/SearchSourceResultsLoadingIndicator.tsx +++ b/src/components/Search/SearchResults/SearchSourceResultsLoadingIndicator.tsx @@ -10,9 +10,13 @@ export const SearchSourceResultsLoadingIndicator = () => { className='str-chat__search-source-results__loading-indicator' data-testid='search-loading-indicator' > - {t('Searching for {{ searchSourceType }}...', { - searchSourceType: searchSource.type, - })} + {t( + 'search.sourceResults.searching.text', + 'Searching for {{ searchSourceType }}...', + { + searchSourceType: searchSource.type, + }, + )}
    ); }; diff --git a/src/components/Search/__tests__/Search.test.tsx b/src/components/Search/__tests__/Search.test.tsx index 2dc3da57a4..b2d6e5db3b 100644 --- a/src/components/Search/__tests__/Search.test.tsx +++ b/src/components/Search/__tests__/Search.test.tsx @@ -16,6 +16,7 @@ import type { } from '../../../context'; import { useStateStore } from '../../../store'; import type { SearchContextValue } from '../SearchContext'; +import { mockT } from '../../../mock-builders/translator'; // vi.mock('../SearchContext'); vi.mock('../../../context'); @@ -23,7 +24,7 @@ vi.mock('../../../store'); const SEARCH_TEST_ID = 'search'; const SEARCH_BAR_TEST_ID = 'search-bar'; -const SEARCH_RESULTS_ARIA_LABEL = 'aria/Search results'; +const SEARCH_RESULTS_ARIA_LABEL = 'Search results'; const CustomSearchBar = () => (
    Custom Search Bar
    @@ -55,7 +56,7 @@ describe('Search', () => { vi.mocked(useTranslationContext).mockReturnValue( fromPartial({ - t: (key) => key, + t: mockT, }), ); diff --git a/src/components/Search/__tests__/SearchBar.test.tsx b/src/components/Search/__tests__/SearchBar.test.tsx index 3f99534300..4699bbb40f 100644 --- a/src/components/Search/__tests__/SearchBar.test.tsx +++ b/src/components/Search/__tests__/SearchBar.test.tsx @@ -10,6 +10,7 @@ import type { TranslationContextValue } from '../../../context'; import { useTranslationContext } from '../../../context'; import { useStateStore } from '../../../store'; import { axe } from '../../../../axe-helper'; +import { mockT } from '../../../mock-builders/translator'; const { announceInteraction } = vi.hoisted(() => ({ announceInteraction: vi.fn() })); @@ -25,7 +26,7 @@ vi.mock('../../Accessibility', () => ({ })); const INPUT_TEST_ID = 'search-input'; -const CLEAR_SEARCH_BUTTON_ARIA_LABEL = 'aria/Clear search'; +const CLEAR_SEARCH_BUTTON_ARIA_LABEL = 'Clear search'; const SEARCH_INPUT_ACCESSIBLE_NAME = 'Search'; describe('SearchBar', () => { @@ -52,7 +53,7 @@ describe('SearchBar', () => { fromPartial(defaultProps), ); vi.mocked(useTranslationContext).mockReturnValue( - fromPartial({ t: (key: any) => key }), + fromPartial({ t: mockT }), ); vi.mocked(useStateStore).mockReturnValue({ isActive: false, diff --git a/src/components/Search/__tests__/SearchResultItem.test.tsx b/src/components/Search/__tests__/SearchResultItem.test.tsx index 965d1590a6..e3217dedbb 100644 --- a/src/components/Search/__tests__/SearchResultItem.test.tsx +++ b/src/components/Search/__tests__/SearchResultItem.test.tsx @@ -23,6 +23,7 @@ import { initClientWithChannels, mockTranslationContextValue, } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; const CHANNEL_PREVIEW_BUTTON_TEST_ID = 'channel-list-item-button'; @@ -41,16 +42,7 @@ vi.mock('../../../context', async (importOriginal) => ({ }), })); -const mockTranslation = (key: string, options?: Record) => { - const interpolated = Object.entries(options || {}).reduce( - (value, [name, arg]) => value.replace(`{{ ${name} }}`, String(arg)), - key, - ); - - return interpolated.startsWith('aria/') - ? interpolated.replace('aria/', '') - : interpolated; -}; +const mockTranslation = mockT; const renderComponent = async ({ activeChannel, diff --git a/src/components/Search/__tests__/SearchResults.test.tsx b/src/components/Search/__tests__/SearchResults.test.tsx index 44082707e0..3c590c491e 100644 --- a/src/components/Search/__tests__/SearchResults.test.tsx +++ b/src/components/Search/__tests__/SearchResults.test.tsx @@ -8,6 +8,7 @@ import type { SearchContextValue } from '../SearchContext'; import { useComponentContext, useTranslationContext } from '../../../context'; import type { TranslationContextValue } from '../../../context'; import { useStateStore } from '../../../store'; +import { mockT } from '../../../mock-builders/translator'; vi.mock('../SearchContext'); vi.mock('../../../context'); @@ -27,7 +28,7 @@ const mockedUseStateStore = vi.mocked(useStateStore); const SOURCE_RESULTS_TEST_ID = 'default-source-results'; const SEARCH_RESULTS_HEADER_TEST_ID = 'default-header'; const PRESEARCH_TEST_ID = 'default-presearch'; -const SEARCH_RESULTS_ARIA_LABEL = 'aria/Search results'; +const SEARCH_RESULTS_ARIA_LABEL = 'Search results'; describe('SearchResults', () => { const mockSearchSource = { isActive: true, @@ -81,7 +82,7 @@ describe('SearchResults', () => { mockedUseTranslationContext.mockReturnValue( fromPartial({ - t: (key) => key, + t: mockT, }), ); diff --git a/src/components/Search/__tests__/SearchResultsHeader.test.tsx b/src/components/Search/__tests__/SearchResultsHeader.test.tsx index bee3576910..1a112b54f7 100644 --- a/src/components/Search/__tests__/SearchResultsHeader.test.tsx +++ b/src/components/Search/__tests__/SearchResultsHeader.test.tsx @@ -5,6 +5,7 @@ import { SearchResultsHeader } from '../SearchResults'; import { useSearchContext } from '../SearchContext'; import { useTranslationContext } from '../../../context'; import { useStateStore } from '../../../store'; +import { mockT } from '../../../mock-builders/translator'; vi.mock('../SearchContext'); vi.mock('../../../context'); @@ -43,7 +44,7 @@ describe('SearchResultsHeader', () => { }); useTranslationContext['mockReturnValue']({ - t: (key) => key, + t: mockT, }); useStateStore['mockReturnValue']({ isActive: false }); @@ -65,24 +66,19 @@ describe('SearchResultsHeader', () => { const buttons = screen.getAllByRole('button'); expect(buttons).toHaveLength(3); - expect( - screen.getByText('search-results-header-filter-source-button-label--channels'), - ).toBeInTheDocument(); - expect( - screen.getByText('search-results-header-filter-source-button-label--messages'), - ).toBeInTheDocument(); - expect( - screen.getByText('search-results-header-filter-source-button-label--users'), - ).toBeInTheDocument(); + expect(screen.getByText('channels')).toBeInTheDocument(); + expect(screen.getByText('messages')).toBeInTheDocument(); + expect(screen.getByText('users')).toBeInTheDocument(); }); it('applies correct aria-labels to all buttons', () => { render(); const buttons = screen.getAllByRole('button'); - buttons.forEach((button) => { - expect(button).toHaveAttribute( + // Each button names its own source, interpolated into the shared label. + ['channels', 'messages', 'users'].forEach((source, index) => { + expect(buttons[index]).toHaveAttribute( 'aria-label', - 'aria/Search results header filter button for: {{ source }}', + `Search results header filter button for: ${source}`, ); }); }); @@ -93,9 +89,7 @@ describe('SearchResultsHeader', () => { useStateStore['mockReturnValue']({ isActive: true }); render(); - const label = screen.getByText( - 'search-results-header-filter-source-button-label--messages', - ); + const label = screen.getByText('messages'); const button = label.closest('button'); expect(button).toHaveClass( 'str-chat__search-results-header__filter-source-button--active', @@ -106,9 +100,7 @@ describe('SearchResultsHeader', () => { useStateStore['mockReturnValue']({ isActive: false }); render(); - const label = screen.getByText( - 'search-results-header-filter-source-button-label--messages', - ); + const label = screen.getByText('messages'); const button = label.closest('button'); expect(button).not.toHaveClass( 'str-chat__search-results-header__filter-source-button--active', @@ -124,9 +116,7 @@ describe('SearchResultsHeader', () => { }); render(); - fireEvent.click( - screen.getByText('search-results-header-filter-source-button-label--messages'), - ); + fireEvent.click(screen.getByText('messages')); expect(mockSearchController.deactivateSource).toHaveBeenCalledWith('messages'); expect(mockSearchController.activateSource).not.toHaveBeenCalled(); @@ -138,9 +128,7 @@ describe('SearchResultsHeader', () => { it('activates and searches source with no items', () => { render(); - fireEvent.click( - screen.getByText('search-results-header-filter-source-button-label--channels'), - ); + fireEvent.click(screen.getByText('channels')); expect(mockSearchController.activateSource).toHaveBeenCalledWith('channels'); expect(mockSources.channels.search).toHaveBeenCalledWith('test query'); @@ -148,9 +136,7 @@ describe('SearchResultsHeader', () => { it('only performs search upon activation if it does not have items loaded', () => { render(); - fireEvent.click( - screen.getByText('search-results-header-filter-source-button-label--messages'), - ); + fireEvent.click(screen.getByText('messages')); expect(mockSearchController.activateSource).toHaveBeenCalledWith('messages'); expect(mockSources.messages.search).not.toHaveBeenCalled(); @@ -160,9 +146,7 @@ describe('SearchResultsHeader', () => { mockSearchController.searchQuery = ''; render(); - fireEvent.click( - screen.getByText('search-results-header-filter-source-button-label--channels'), - ); + fireEvent.click(screen.getByText('channels')); expect(mockSearchController.activateSource).toHaveBeenCalledWith('channels'); expect(mockSources.channels.search).not.toHaveBeenCalled(); }); diff --git a/src/components/Search/__tests__/SearchSourceResultListFooter.test.tsx b/src/components/Search/__tests__/SearchSourceResultListFooter.test.tsx index 37cbc503b6..1c1a99e58f 100644 --- a/src/components/Search/__tests__/SearchSourceResultListFooter.test.tsx +++ b/src/components/Search/__tests__/SearchSourceResultListFooter.test.tsx @@ -7,6 +7,7 @@ import { useSearchSourceResultsContext } from '../SearchSourceResultsContext'; import type { ComponentContextValue, TranslationContextValue } from '../../../context'; import { useComponentContext, useTranslationContext } from '../../../context'; import { useStateStore } from '../../../store'; +import { mockT } from '../../../mock-builders/translator'; vi.mock('../SearchSourceResultsContext'); vi.mock('../../../context'); @@ -40,7 +41,7 @@ describe('SearchSourceResultListFooter', () => { vi.mocked(useTranslationContext).mockReturnValue( fromPartial({ - t: (key: any) => key, + t: mockT, }), ); @@ -110,7 +111,9 @@ describe('SearchSourceResultListFooter', () => { }); it('translates "All results loaded" message', () => { - const mockTranslate = vi.fn((key: any) => `Translated ${key}`); + const mockTranslate = vi.fn( + (key: any, defaultValue?: any) => `Translated ${defaultValue ?? key}`, + ); vi.mocked(useTranslationContext).mockReturnValue( fromPartial({ t: mockTranslate }), ); @@ -122,7 +125,10 @@ describe('SearchSourceResultListFooter', () => { render(); - expect(mockTranslate).toHaveBeenCalledWith('All results loaded'); + expect(mockTranslate).toHaveBeenCalledWith( + 'common.resultsLoaded.label', + 'All results loaded', + ); expect(screen.getByText('Translated All results loaded')).toBeInTheDocument(); }); diff --git a/src/components/SummarizedMessagePreview/__tests__/useLatestMessagePreview.test.tsx b/src/components/SummarizedMessagePreview/__tests__/useLatestMessagePreview.test.tsx index 228b278975..3b1b47d500 100644 --- a/src/components/SummarizedMessagePreview/__tests__/useLatestMessagePreview.test.tsx +++ b/src/components/SummarizedMessagePreview/__tests__/useLatestMessagePreview.test.tsx @@ -323,7 +323,7 @@ describe('useLatestMessagePreview', () => { }); const { result } = renderPreviewHook({ latestMessage: message }); expect(result.current.type).toBe('image'); - expect(result.current.text).toBe('imageCount'); + expect(result.current.text).toBe('2 images'); }); it('uses file type for mixed attachment types', () => { @@ -334,7 +334,7 @@ describe('useLatestMessagePreview', () => { }); const { result } = renderPreviewHook({ latestMessage: message }); expect(result.current.type).toBe('file'); - expect(result.current.text).toBe('fileCount'); + expect(result.current.text).toBe('2 files'); }); // v10: attachment-specific fields such as `duration` live under `attachment.custom`. @@ -366,7 +366,7 @@ describe('useLatestMessagePreview', () => { const { result } = renderPreviewHook({ latestMessage: message }); expect(result.current.type).toBe('voice'); // voice recordings use generic fallback (voiceMessageCount) since fallback text is not useful - expect(result.current.text).toBe('voiceMessageCount (1:04)'); + expect(result.current.text).toBe('Voice message (1:04)'); }); it('formats zero-second duration correctly', () => { diff --git a/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts b/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts index 98ea0f4be4..336d34bc11 100644 --- a/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts +++ b/src/components/SummarizedMessagePreview/hooks/useLatestMessagePreview.ts @@ -102,19 +102,43 @@ function getAttachmentFallbackText( ): string { switch (type) { case 'image': - return t('imageCount', { count }); + return t('messagePreview.latestMessagePreview.imageCount.label', { + count, + defaultValue_one: 'Image', + defaultValue_other: '{{ count }} images', + }); case 'video': - return t('videoCount', { count }); + return t('messagePreview.latestMessagePreview.videoCount.label', { + count, + defaultValue_one: 'Video', + defaultValue_other: '{{ count }} videos', + }); case 'voice': - return t('voiceMessageCount', { count }); + return t('messagePreview.latestMessagePreview.voiceMessageCount.label', { + count, + defaultValue_one: 'Voice message', + defaultValue_other: '{{ count }} voice messages', + }); case 'link': - return t('linkCount', { count }); + return t('messagePreview.latestMessagePreview.linkCount.label', { + count, + defaultValue_one: 'Link', + defaultValue_other: '{{ count }} links', + }); case 'file': - return t('fileCount', { count }); + return t('messagePreview.latestMessagePreview.fileCount.label', { + count, + defaultValue_one: 'File', + defaultValue_other: '{{ count }} files', + }); case 'unsupported': - return t('Unsupported attachment'); + return t('common.unsupportedAttachment.text', 'Unsupported attachment'); default: - return t('fileCount', { count }); + return t('messagePreview.latestMessagePreview.fileCount.label', { + count, + defaultValue_one: 'File', + defaultValue_other: '{{ count }} files', + }); } } @@ -141,11 +165,20 @@ export const useLatestMessagePreview = ({ return useMemo(() => { if (!latestMessage) { - return { text: t('Nothing yet...'), type: 'empty' as const }; + return { + text: t('common.nothingYet.text', 'Nothing yet...'), + type: 'empty' as const, + }; } if (latestMessage.status === 'failed' || latestMessage.type === 'error') { - return { text: t('Message failed to send'), type: 'error' as const }; + return { + text: t( + 'messagePreview.latestMessagePreview.messageFailedSend.text', + 'Message failed to send', + ), + type: 'error' as const, + }; } const isOwnMessage = latestMessage.user?.id === client.user?.id; @@ -157,7 +190,7 @@ export const useLatestMessagePreview = ({ let senderName: string | undefined; if (isOwnMessage) { - senderName = t('You'); + senderName = t('common.you.label', 'You'); } else if (!isOwnMessage && participantCount !== undefined && participantCount > 2) { senderName = latestMessage.user?.name || latestMessage.user?.id; } @@ -166,7 +199,7 @@ export const useLatestMessagePreview = ({ return { deliveryStatus, senderName, - text: t('Message deleted'), + text: t('common.messageDeleted.text', 'Message deleted'), type: 'deleted' as const, }; } @@ -175,7 +208,7 @@ export const useLatestMessagePreview = ({ return { deliveryStatus, senderName, - text: t('Poll'), + text: t('common.poll.label', 'Poll'), type: 'poll' as const, }; } @@ -188,7 +221,7 @@ export const useLatestMessagePreview = ({ return { deliveryStatus, senderName, - text: textContent || t('Location'), + text: textContent || t('common.location.text', 'Location'), type: 'location' as const, }; } @@ -255,7 +288,10 @@ export const useLatestMessagePreview = ({ }; } - return { text: t('Empty message...'), type: 'empty' as const }; + return { + text: t('common.emptyMessage.text', 'Empty message...'), + type: 'empty' as const, + }; }, [ client.user?.id, latestMessage, diff --git a/src/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.tsx b/src/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.tsx index d6f1e7105f..f9e0aedcaa 100644 --- a/src/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.tsx +++ b/src/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.tsx @@ -19,8 +19,8 @@ export const BroadcastMentionItem = ({ const { t } = useTranslationContext(); const description = entity.mentionType === 'channel' - ? t('mention/Channel Description') - : t('mention/Here Description'); + ? t('mention.channel.description', 'Notify everyone in this channel') + : t('mention.here.description', 'Notify every online member in this channel'); return ( title: `@${role}`, }} selected={focused} - subtitle={t('Notify all {{ role }} members', { role })} + subtitle={t( + 'textareaComposer.roleItem.notifyMembers.label', + 'Notify all {{ role }} members', + { role }, + )} subtitleClassName='str-chat__suggestion-list__item-details' title={ diff --git a/src/components/TextareaComposer/SuggestionList/SuggestionList.tsx b/src/components/TextareaComposer/SuggestionList/SuggestionList.tsx index 1eefd06918..14ef7086e9 100644 --- a/src/components/TextareaComposer/SuggestionList/SuggestionList.tsx +++ b/src/components/TextareaComposer/SuggestionList/SuggestionList.tsx @@ -260,13 +260,22 @@ export const SuggestionList = ({ const suggestionMenuLabel = useMemo(() => { switch (suggestions?.searchSource.type) { case 'commands': - return t('aria/Command Suggestions'); + return t( + 'textareaComposer.suggestionList.commandSuggestions.ariaLabel', + 'Command Suggestions', + ); case 'emoji': - return t('aria/Emoji Suggestions'); + return t( + 'textareaComposer.suggestionList.emojiSuggestions.ariaLabel', + 'Emoji Suggestions', + ); case 'mentions': - return t('aria/Mention Suggestions'); + return t( + 'textareaComposer.suggestionList.mentionSuggestions.ariaLabel', + 'Mention Suggestions', + ); default: - return t('aria/Suggestions'); + return t('textareaComposer.suggestionList.suggestions.ariaLabel', 'Suggestions'); } }, [suggestions?.searchSource.type, t]); diff --git a/src/components/TextareaComposer/TextareaComposer.tsx b/src/components/TextareaComposer/TextareaComposer.tsx index 23ee470ffa..33b913bd3a 100644 --- a/src/components/TextareaComposer/TextareaComposer.tsx +++ b/src/components/TextareaComposer/TextareaComposer.tsx @@ -146,7 +146,9 @@ const TextareaComposerWithLiveAnnouncements = ({ // to a stable label instead of the placeholder, which may be a command-specific // template (e.g. mention/command args) that would otherwise be re-announced as a // stale name even though the field already holds real content. - const ariaLabel = text ? t('aria/Message input') : placeholder; + const ariaLabel = text + ? t('textareaComposer.messageInput.ariaLabel', 'Message input') + : placeholder; // react-textarea-autosize can measure placeholder content as multi-line in narrow layouts, // producing an inflated initial height (e.g. 2 rows) before the user types. diff --git a/src/components/TextareaComposer/__tests__/CommandItem.test.tsx b/src/components/TextareaComposer/__tests__/CommandItem.test.tsx index 5dffad064e..a0c34c6263 100644 --- a/src/components/TextareaComposer/__tests__/CommandItem.test.tsx +++ b/src/components/TextareaComposer/__tests__/CommandItem.test.tsx @@ -23,7 +23,25 @@ vi.mock('../../MessageComposer/hooks', () => ({ vi.mock('../../../context', () => ({ useTranslationContext: () => ({ - t: (key: string) => key, + t: (key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }, }), })); diff --git a/src/components/TextareaComposer/__tests__/MentionItem.test.tsx b/src/components/TextareaComposer/__tests__/MentionItem.test.tsx index 1d6ee31a85..94f2b73927 100644 --- a/src/components/TextareaComposer/__tests__/MentionItem.test.tsx +++ b/src/components/TextareaComposer/__tests__/MentionItem.test.tsx @@ -13,6 +13,7 @@ import type { } from '../SuggestionList'; import { MentionItem } from '../SuggestionList'; import { mockTranslationContextValue } from '../../../mock-builders'; +import { mockT } from '../../../mock-builders/translator'; afterEach(cleanup); @@ -21,10 +22,7 @@ describe('MentionItem', () => { const { container, getByRole, getByText, queryByTestId } = render( - key === 'mention/Channel Description' - ? 'Notify everyone in this channel' - : key, + t: mockT, })} >
    @@ -54,10 +52,7 @@ describe('MentionItem', () => { const { getByText } = render( - key === 'mention/Here Description' - ? 'Notify every online member in this channel' - : key, + t: mockT, })} >
    @@ -81,10 +76,7 @@ describe('MentionItem', () => { const { container, getByRole, getByText, queryByTestId } = render( ) => - key === 'Notify all {{ role }} members' && options?.role - ? `Notify all ${options.role} members` - : key, + t: mockT, })} >
    diff --git a/src/components/TextareaComposer/__tests__/SuggestionList.test.tsx b/src/components/TextareaComposer/__tests__/SuggestionList.test.tsx index 0955911d0b..ae8768243a 100644 --- a/src/components/TextareaComposer/__tests__/SuggestionList.test.tsx +++ b/src/components/TextareaComposer/__tests__/SuggestionList.test.tsx @@ -32,7 +32,25 @@ vi.mock('../../../context', async (importOriginal) => ({ textareaRef: { current: null }, }), useTranslationContext: () => ({ - t: (key: string) => key, + t: (key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }, }), })); @@ -152,7 +170,7 @@ describe('SuggestionList', () => { expect(announceInteractionMock).toHaveBeenCalledWith('suggestions.count', { count: 3, - suggestionsLabel: 'aria/Mention Suggestions', // mentions → localized type label (t mock returns the key) + suggestionsLabel: 'Mention Suggestions', // mentions → localized type label (t mock returns the key) }); }); @@ -161,7 +179,7 @@ describe('SuggestionList', () => { expect(announceInteractionMock).toHaveBeenLastCalledWith('suggestions.count', { count: 3, - suggestionsLabel: 'aria/Mention Suggestions', + suggestionsLabel: 'Mention Suggestions', }); fakeComposerState = buildState(1); @@ -173,7 +191,7 @@ describe('SuggestionList', () => { expect(announceInteractionMock).toHaveBeenLastCalledWith('suggestions.count', { count: 1, - suggestionsLabel: 'aria/Mention Suggestions', + suggestionsLabel: 'Mention Suggestions', }); }); @@ -195,7 +213,7 @@ describe('SuggestionList', () => { expect(announceInteractionMock).toHaveBeenCalledTimes(2); expect(announceInteractionMock).toHaveBeenLastCalledWith('suggestions.count', { count: 3, - suggestionsLabel: 'aria/Mention Suggestions', + suggestionsLabel: 'Mention Suggestions', }); }); @@ -204,7 +222,7 @@ describe('SuggestionList', () => { renderSuggestionList(); expect(announceInteractionMock).toHaveBeenLastCalledWith('suggestions.count', { count: 2, - suggestionsLabel: 'aria/Emoji Suggestions', + suggestionsLabel: 'Emoji Suggestions', }); }); @@ -213,7 +231,7 @@ describe('SuggestionList', () => { renderSuggestionList(); expect(announceInteractionMock).toHaveBeenLastCalledWith('suggestions.count', { count: 2, - suggestionsLabel: 'aria/Suggestions', + suggestionsLabel: 'Suggestions', }); }); @@ -231,7 +249,7 @@ describe('SuggestionList', () => { const listbox = container.querySelector('[role="listbox"]'); expect(listbox).toBeInTheDocument(); // The listbox carries the localized type label as its accessible name. - expect(listbox).toHaveAttribute('aria-label', 'aria/Mention Suggestions'); + expect(listbox).toHaveAttribute('aria-label', 'Mention Suggestions'); const options = container.querySelectorAll('[role="option"]'); expect(options.length).toBe(3); diff --git a/src/components/TextareaComposer/hooks/useTextareaPlaceholder.ts b/src/components/TextareaComposer/hooks/useTextareaPlaceholder.ts index d2f472c476..2227857129 100644 --- a/src/components/TextareaComposer/hooks/useTextareaPlaceholder.ts +++ b/src/components/TextareaComposer/hooks/useTextareaPlaceholder.ts @@ -25,11 +25,11 @@ export const useTextareaPlaceholder = ({ const knownArgsTranslations = useMemo>( () => ({ - ban: t('ban-command-args'), - giphy: t('giphy-command-args'), - mute: t('mute-command-args'), - unban: t('unban-command-args'), - unmute: t('unmute-command-args'), + ban: t('command.ban.args', '[@username] [text]'), + giphy: t('command.giphy.args', '[text]'), + mute: t('command.mute.args', '[@username]'), + unban: t('command.unban.args', '[@username]'), + unmute: t('command.unmute.args', '[@username]'), }), [t], ); @@ -37,13 +37,21 @@ export const useTextareaPlaceholder = ({ const commandArgs = command?.args && (knownArgsTranslations[command.name ?? ''] ?? t(command.args)); const commandPlaceholder = - command?.name === 'giphy' ? t('Search GIFs') : (commandArgs ?? undefined); + command?.name === 'giphy' + ? t('textareaComposer.textareaPlaceholder.searchGiFs.label', 'Search GIFs') + : (commandArgs ?? undefined); const defaultPlaceholder = - placeholder ?? additionalTextareaProps?.placeholder ?? t('Send a message'); + placeholder ?? + additionalTextareaProps?.placeholder ?? + t('textareaComposer.textareaPlaceholder.sendMessage.label', 'Send a message'); if (cooldownRemaining) { - return t('Slow mode, wait {{ seconds }}s...', { seconds: cooldownRemaining }); + return t( + 'textareaComposer.textareaPlaceholder.slowModeWaitS.label', + 'Slow mode, wait {{ seconds }}s...', + { seconds: cooldownRemaining }, + ); } return commandPlaceholder ?? defaultPlaceholder; diff --git a/src/components/Thread/ThreadHeader.tsx b/src/components/Thread/ThreadHeader.tsx index 664155baf9..85329f7305 100644 --- a/src/components/Thread/ThreadHeader.tsx +++ b/src/components/Thread/ThreadHeader.tsx @@ -47,7 +47,11 @@ const ThreadHeaderSubtitle = ({ ({ parent_id, user }) => user?.id !== client.user?.id && parent_id === parentId, ); const hasTyping = channelConfig?.typing_events !== false && typingInThread.length > 0; - const replyCountText = t('replyCount', { count: replyCount ?? 0 }); + const replyCountText = t('common.replyCount.label', { + count: replyCount ?? 0, + defaultValue_one: '1 reply', + defaultValue_other: '{{ count }} replies', + }); const defaultSubtitle = threadDisplayName ? `${threadDisplayName} · ${replyCountText}` : replyCountText; @@ -112,7 +116,9 @@ export const ThreadHeader = (props: ThreadHeaderProps) => { {isThreadsView && HeaderStartContent && }
    -
    {t('Thread')}
    +
    + {t('thread.header.thread.text', 'Thread')} +
    {
    diff --git a/src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx b/src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx index da3dbfeac1..3c5ebe442f 100644 --- a/src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx +++ b/src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx @@ -5,6 +5,7 @@ import type { StreamChat } from 'stream-chat'; import { ThreadList } from '../ThreadList'; import { initClientWithChannels } from '../../../../mock-builders'; +import { mockT } from '../../../../mock-builders/translator'; const mockUseChatContext = vi.fn(); const mockUseComponentContext = vi.fn(); @@ -95,7 +96,7 @@ describe('ThreadList', () => { vi.spyOn(client.threads, 'reload').mockResolvedValue(undefined); mockUseChatContext.mockReturnValue({ client }); mockUseComponentContext.mockReturnValue({}); - mockUseTranslationContext.mockReturnValue({ t: (value: string) => value }); + mockUseTranslationContext.mockReturnValue({ t: mockT }); mockUseStateStore.mockReturnValue({ isLoading: false, threads: [] }); }); @@ -129,7 +130,7 @@ describe('ThreadList', () => { expect(screen.queryByTestId('loading-channels')).not.toBeInTheDocument(); expect(mockVirtuoso).toHaveBeenCalledTimes(1); expect(mockVirtuoso.mock.calls[0][0]).toMatchObject({ - 'aria-label': 'aria/Thread list', + 'aria-label': 'Thread list', role: 'listbox', }); }); diff --git a/src/components/Threads/ThreadList/__tests__/ThreadListHeader.test.tsx b/src/components/Threads/ThreadList/__tests__/ThreadListHeader.test.tsx index a7f2307693..1fd3b9ca83 100644 --- a/src/components/Threads/ThreadList/__tests__/ThreadListHeader.test.tsx +++ b/src/components/Threads/ThreadList/__tests__/ThreadListHeader.test.tsx @@ -5,8 +5,9 @@ import { WithComponents, WorkspaceNavigationProvider } from '../../../../context import { TranslationProvider } from '../../../../context/TranslationContext'; import { mockTranslationContextValue } from '../../../../mock-builders'; import { ThreadListHeader } from '../ThreadListHeader'; +import { mockT } from '../../../../mock-builders/translator'; -const t = vi.fn((key: string) => key); +const t = vi.fn(mockT); const HeaderEndContent = () =>
    ; afterEach(cleanup); diff --git a/src/components/Threads/ThreadList/__tests__/ThreadListItemUI.test.tsx b/src/components/Threads/ThreadList/__tests__/ThreadListItemUI.test.tsx index e2604f2ac3..7d22f0d6da 100644 --- a/src/components/Threads/ThreadList/__tests__/ThreadListItemUI.test.tsx +++ b/src/components/Threads/ThreadList/__tests__/ThreadListItemUI.test.tsx @@ -3,6 +3,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { axe } from '../../../../../axe-helper'; import { ThreadListItemUI } from '../ThreadListItemUI'; +import { mockT } from '../../../../mock-builders/translator'; const { announceInteraction } = vi.hoisted(() => ({ announceInteraction: vi.fn() })); @@ -77,16 +78,7 @@ describe('ThreadListItemUI', () => { beforeEach(() => { mockUseChatContext.mockReturnValue({ client: { userID: 'martin' } }); mockUseTranslationContext.mockReturnValue({ - t: (key: string, values?: Record) => { - if (key === 'replyCount') return `${values?.count ?? 0} replies`; - const interpolated = Object.entries(values ?? {}).reduce( - (value, [name, arg]) => value.replace(`{{ ${name} }}`, String(arg)), - key, - ); - return interpolated.startsWith('aria/') - ? interpolated.replace('aria/', '') - : interpolated; - }, + t: mockT, tDateTimeParser: () => 'recently', userLanguage: 'en', }); diff --git a/src/components/Threads/ThreadList/__tests__/utils.a11y.test.ts b/src/components/Threads/ThreadList/__tests__/utils.a11y.test.ts index ce321cdbd3..a319664afa 100644 --- a/src/components/Threads/ThreadList/__tests__/utils.a11y.test.ts +++ b/src/components/Threads/ThreadList/__tests__/utils.a11y.test.ts @@ -3,23 +3,13 @@ import { fromPartial } from '@total-typescript/shoehorn'; import type { LocalMessage, StreamChat } from 'stream-chat'; import type { TranslationContextValue } from '../../../../context/TranslationContext'; +import { mockT } from '../../../../mock-builders/translator'; import { composeThreadListItemAccessibleLabel, DEFAULT_THREAD_LIST_ITEM_LABEL_ORDER, } from '../utils.a11y'; -// Mirrors the natural-language fallback: interpolate {{ name }} and drop the `aria/` prefix. The -// `replyCount` plural key carries no placeholder in the key itself, so special-case it. -const t = ((key: string, opts?: Record) => { - if (key === 'replyCount') return `${opts?.count} replies`; - const interpolated = Object.entries(opts ?? {}).reduce( - (value, [name, arg]) => value.replace(`{{ ${name} }}`, String(arg)), - key, - ); - return interpolated.startsWith('aria/') - ? interpolated.replace('aria/', '') - : interpolated; -}) as TranslationContextValue['t']; +const t = mockT as TranslationContextValue['t']; const tDateTimeParser = (() => 'recently') as unknown as TranslationContextValue['tDateTimeParser']; @@ -40,7 +30,7 @@ const baseData = { describe('composeThreadListItemAccessibleLabel', () => { it('composes name, unread, parent message, reply count, and time in reading order', () => { expect(composeThreadListItemAccessibleLabel(baseData)).toBe( - 'Chat: General. 2 unread message. Thread: hello world. 3 replies. Last activity: recently', + 'Chat: General. 2 unread messages. Thread: hello world. 3 replies. Last activity: recently', ); }); diff --git a/src/components/Threads/ThreadList/utils.a11y.ts b/src/components/Threads/ThreadList/utils.a11y.ts index 8a24340f49..2dc40eb3f7 100644 --- a/src/components/Threads/ThreadList/utils.a11y.ts +++ b/src/components/Threads/ThreadList/utils.a11y.ts @@ -61,7 +61,9 @@ export const defaultThreadListItemLabelParts = { active: activeLabelPart, name: ({ displayTitle, t }) => displayTitle - ? t('aria/Chat: {{ channelName }}', { channelName: displayTitle }) + ? t('threadList.chat.ariaLabel', 'Chat: {{ channelName }}', { + channelName: displayTitle, + }) : undefined, // The message the thread is about (shown as the row's subtitle); same preview the subtitle shows. parentMessage: ({ parentMessagePreview, parentMessageSender, t }) => { @@ -69,11 +71,17 @@ export const defaultThreadListItemLabelParts = { const preview = parentMessageSender ? `${parentMessageSender}: ${parentMessagePreview}` : parentMessagePreview; - return t('aria/Thread: {{ messagePreview }}', { messagePreview: preview }); + return t('threadList.thread.ariaLabel', 'Thread: {{ messagePreview }}', { + messagePreview: preview, + }); }, replyCount: ({ replyCount, t }) => typeof replyCount === 'number' && replyCount > 0 - ? t('replyCount', { count: replyCount }) + ? t('common.replyCount.label', { + count: replyCount, + defaultValue_one: '1 reply', + defaultValue_other: '{{ count }} replies', + }) : undefined, time: ({ latestReply, t, tDateTimeParser }) => { const createdAt = latestReply?.created_at; @@ -82,9 +90,13 @@ export const defaultThreadListItemLabelParts = { messageCreatedAt: createdAt.toISOString(), t, tDateTimeParser, - timestampTranslationKey: 'timestamp/ChannelPreviewTimestamp', + timestampTranslationKey: 'timestamp.ChannelPreviewTimestamp', }); - return when ? t('aria/Last activity: {{ time }}', { time: String(when) }) : undefined; + return when + ? t('common.lastActivity.ariaLabel', 'Last activity: {{ time }}', { + time: String(when), + }) + : undefined; }, unreadCount: unreadCountLabelPart, } satisfies Record; diff --git a/src/components/TypingIndicator/__tests__/TypingIndicator.test.tsx b/src/components/TypingIndicator/__tests__/TypingIndicator.test.tsx index 2f0d7495e7..e229578dab 100644 --- a/src/components/TypingIndicator/__tests__/TypingIndicator.test.tsx +++ b/src/components/TypingIndicator/__tests__/TypingIndicator.test.tsx @@ -25,7 +25,7 @@ import { // config from `client.configsStore`. The former TypingContext/ChannelStateContext providers and // scrollToBottom/threadList props are gone. The visible text is now a visually-hidden // `typing-indicator-status` live region whose content comes from `getTypingStatusMessage` -// ('{{ typing }} is typing' / '{{ typing }} are typing' / '{{ count }} people are typing'), so +// ('jessica is typing' / 'jessica and joris are typing' / '3 people are typing'), so // assertions match those keys plus the AvatarStack (capped at 3, with a "+N" overflow badge). // (This replaces the stale TypingIndicator.test.js, whose JSX-in-.js content could not be parsed.) @@ -137,7 +137,7 @@ describe('TypingIndicator', () => { expect(container.firstChild).toHaveClass('str-chat__typing-indicator--typing'); expect(screen.getByTestId('typing-indicator-status')).toHaveTextContent( - '{{ typing }} is typing', + 'jessica is typing', ); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -153,7 +153,7 @@ describe('TypingIndicator', () => { expect(container.firstChild).toHaveClass('str-chat__typing-indicator--typing'); // Own typing entry is filtered out, so a single (foreign) typer remains. expect(screen.getByTestId('typing-indicator-status')).toHaveTextContent( - '{{ typing }} is typing', + 'jessica is typing', ); expect(screen.getAllByTestId('avatar')).toHaveLength(1); const results = await axe(container); @@ -167,7 +167,7 @@ describe('TypingIndicator', () => { joris: { user: { id: 'joris', image: 'joris.jpg' } }, }); expect(screen.getByTestId('typing-indicator-status')).toHaveTextContent( - '{{ typing }} are typing', + 'jessica and joris are typing', ); expect(screen.getAllByTestId('avatar')).toHaveLength(2); const results = await axe(container); @@ -182,7 +182,7 @@ describe('TypingIndicator', () => { margriet: { user: { id: 'margriet', image: 'margriet.jpg' } }, }); expect(screen.getByTestId('typing-indicator-status')).toHaveTextContent( - '{{ count }} people are typing', + '3 people are typing', ); // 3 foreign typers == AvatarStack cap, so all avatars render without an overflow badge. expect(screen.getAllByTestId('avatar')).toHaveLength(3); @@ -200,7 +200,7 @@ describe('TypingIndicator', () => { margriet: { user: { id: 'margriet', image: 'margriet.jpg' } }, }); expect(screen.getByTestId('typing-indicator-status')).toHaveTextContent( - '{{ count }} people are typing', + '4 people are typing', ); // 4 foreign typers exceed the AvatarStack cap of 3 -> overflow badge for the remainder. expect(screen.getAllByTestId('avatar')).toHaveLength(3); diff --git a/src/components/TypingIndicator/utils/__tests__/getTypingStatusMessage.test.ts b/src/components/TypingIndicator/utils/__tests__/getTypingStatusMessage.test.ts index 18038801a1..d8944bf3f7 100644 --- a/src/components/TypingIndicator/utils/__tests__/getTypingStatusMessage.test.ts +++ b/src/components/TypingIndicator/utils/__tests__/getTypingStatusMessage.test.ts @@ -1,12 +1,9 @@ import { getTypingStatusMessage } from '../getTypingStatusMessage'; import type { TypingEntry } from '../../hooks/useDebouncedTypingActive'; +import { mockT } from '../../../../mock-builders/translator'; -const translate = (value: string, options?: Record) => - Object.entries(options || {}).reduce( - (result, [key, optionValue]) => result.replace(`{{ ${key} }}`, String(optionValue)), - value, - ); +const translate = mockT as unknown as Parameters[1]; describe('getTypingStatusMessage', () => { it('formats a single typing user', () => { diff --git a/src/components/TypingIndicator/utils/getTypingStatusMessage.ts b/src/components/TypingIndicator/utils/getTypingStatusMessage.ts index 3c82382d53..977fad817d 100644 --- a/src/components/TypingIndicator/utils/getTypingStatusMessage.ts +++ b/src/components/TypingIndicator/utils/getTypingStatusMessage.ts @@ -1,16 +1,12 @@ +import type { TranslationContextValue } from '../../../context/TranslationContext'; import type { TypingEntry } from '../hooks/useDebouncedTypingActive'; -type TranslationFunction = ( - key: string, - options?: Record, -) => string; - /** * Build a localized typing-status message for screen-reader and inline indicator text. */ export const getTypingStatusMessage = ( displayUsers: readonly TypingEntry[], - t: TranslationFunction, + t: TranslationContextValue['t'], ) => { const namedUsers = displayUsers .map(({ user }) => user?.name?.trim() || user?.id || '') @@ -18,14 +14,18 @@ export const getTypingStatusMessage = ( const count = displayUsers.length; if (count === 1 && namedUsers.length === 1) { - return t('{{ typing }} is typing', { typing: namedUsers[0] }); + return t('typing.singleUser', '{{ typing }} is typing', { typing: namedUsers[0] }); } if (count === 2 && namedUsers.length === 2) { - return t('{{ typing }} are typing', { + return t('typing.twoUsers', '{{ typing }} are typing', { typing: `${namedUsers[0]} and ${namedUsers[1]}`, }); } - return t('{{ count }} people are typing', { count }); + return t('typing.manyUsers', { + count, + defaultValue_one: '{{ count }} person is typing', + defaultValue_other: '{{ count }} people are typing', + }); }; diff --git a/src/components/VideoPlayer/VideoThumbnail.tsx b/src/components/VideoPlayer/VideoThumbnail.tsx index 724fb2a3ac..8b0471071b 100644 --- a/src/components/VideoPlayer/VideoThumbnail.tsx +++ b/src/components/VideoPlayer/VideoThumbnail.tsx @@ -25,7 +25,7 @@ export const VideoThumbnail = ({ {onPlay ? ( } onChange={handleSearchChange} - placeholder={t('Search')} + placeholder={t('common.search.ariaLabel', 'Search')} type='search' value={searchInput} /> diff --git a/src/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.tsx b/src/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.tsx index ef4fc40cb4..dffa6af84b 100644 --- a/src/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.tsx +++ b/src/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.tsx @@ -27,7 +27,10 @@ export const SectionNavigatorHeader = (props: SectionNavigatorHeaderProps) => { return (
    diff --git a/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsx b/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsx index 3c9c8273a3..7847cffdcf 100644 --- a/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsx @@ -184,7 +184,10 @@ export const ChannelFilesView: React.ComponentType = () = return (
    - + ({ searchSourceActivate: vi.fn(), @@ -192,7 +193,7 @@ describe('ChannelFilesView', () => { mocks.searchSourceOptions.length = 0; vi.mocked(useTranslationContext).mockReturnValue({ - t: (key: string) => key, + t: mockT, tDateTimeParser: (input?: string | number | Date) => Dayjs(input), } as unknown as ReturnType); diff --git a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx index 90ef9fb37a..3cc2024d1a 100644 --- a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx @@ -212,7 +212,7 @@ const ChannelMuteAction = () => { addNotification({ context: { channel: targetChannel }, emitter: 'ChannelManagementView', - message: t('Channel unmuted'), + message: t('common.channelUnmuted.text', 'Channel unmuted'), severity: 'success', type: 'api:channel:unmute:success', }), @@ -227,7 +227,10 @@ const ChannelMuteAction = () => { context: { channel: targetChannel }, emitter: 'ChannelManagementView', error: toError(error), - message: t('Error unmuting channel'), + message: t( + 'channelDetail.channelManagementActions.errorUnmutingChannel.text', + 'Error unmuting channel', + ), severity: 'error', type: 'api:channel:unmute:failed', }); @@ -240,7 +243,7 @@ const ChannelMuteAction = () => { addNotification({ context: { channel: targetChannel }, emitter: 'ChannelManagementView', - message: t('Channel muted'), + message: t('common.channelMuted.text', 'Channel muted'), severity: 'success', type: 'api:channel:mute:success', }), @@ -252,7 +255,10 @@ const ChannelMuteAction = () => { context: { channel: targetChannel }, emitter: 'ChannelManagementView', error: toError(error), - message: t('Error muting channel'), + message: t( + 'channelDetail.channelManagementActions.errorMutingChannel.text', + 'Error muting channel', + ), severity: 'error', type: 'api:channel:mute:failed', }); @@ -304,7 +310,11 @@ const ChannelMuteAction = () => { LeadingIcon={optimisticChannelMuted ? MutedActionIcon : MuteActionIcon} RootElement='button' rootProps={rootProps} - title={optimisticChannelMuted ? t('Unmute chat') : t('Mute chat')} + title={ + optimisticChannelMuted + ? t('channelDetail.channelManagementActions.unmuteChat.title', 'Unmute chat') + : t('channelDetail.channelManagementActions.muteChat.title', 'Mute chat') + } TrailingSlot={TrailingSlot} /> ); @@ -335,7 +345,10 @@ const UserMuteAction = () => { addNotification({ context: { channel }, emitter: 'ChannelManagementView', - message: t('User unmuted'), + message: t( + 'channelDetail.channelManagementActions.userUnmuted.text', + 'User unmuted', + ), severity: 'success', type: 'api:user:unmute:success', }), @@ -350,7 +363,10 @@ const UserMuteAction = () => { context: { channel }, emitter: 'ChannelManagementView', error: toError(error), - message: t('Error unmuting user'), + message: t( + 'channelDetail.channelManagementActions.errorUnmutingUser.text', + 'Error unmuting user', + ), severity: 'error', type: 'api:user:unmute:failed', }); @@ -363,7 +379,10 @@ const UserMuteAction = () => { addNotification({ context: { channel }, emitter: 'ChannelManagementView', - message: t('User muted'), + message: t( + 'channelDetail.channelManagementActions.userMuted.text', + 'User muted', + ), severity: 'success', type: 'api:user:mute:success', }), @@ -375,7 +394,10 @@ const UserMuteAction = () => { context: { channel }, emitter: 'ChannelManagementView', error: toError(error), - message: t('Error muting user'), + message: t( + 'channelDetail.channelManagementActions.errorMutingUser.text', + 'Error muting user', + ), severity: 'error', type: 'api:user:mute:failed', }); @@ -426,7 +448,11 @@ const UserMuteAction = () => { LeadingIcon={optimisticUserMuted ? MutedActionIcon : MuteActionIcon} RootElement='button' rootProps={rootProps} - title={optimisticUserMuted ? t('Unmute user') : t('Mute user')} + title={ + optimisticUserMuted + ? t('channelDetail.channelManagementActions.unmuteUser.title', 'Unmute user') + : t('channelDetail.channelManagementActions.muteUser.title', 'Mute user') + } TrailingSlot={TrailingSlot} /> ); @@ -468,7 +494,7 @@ const BlockUserAction = () => { addNotification({ context: { channel }, emitter: 'ChannelManagementView', - message: t('User unblocked'), + message: t('common.userUnblocked.text', 'User unblocked'), severity: 'success', type: 'api:user:unblock:success', }); @@ -477,7 +503,10 @@ const BlockUserAction = () => { context: { channel }, emitter: 'ChannelManagementView', error: toError(error), - message: t('Error unblocking user'), + message: t( + 'channelDetail.channelManagementActions.errorUnblockingUser.text', + 'Error unblocking user', + ), severity: 'error', type: 'api:user:unblock:failed', }); @@ -496,7 +525,7 @@ const BlockUserAction = () => { addNotification({ context: { channel }, emitter: 'ChannelManagementView', - message: t('User blocked'), + message: t('common.userBlocked.text', 'User blocked'), severity: 'success', type: 'api:user:block:success', }); @@ -505,7 +534,10 @@ const BlockUserAction = () => { context: { channel }, emitter: 'ChannelManagementView', error: toError(error), - message: t('Error blocking user'), + message: t( + 'channelDetail.channelManagementActions.errorBlockingUser.text', + 'Error blocking user', + ), severity: 'error', type: 'api:user:block:failed', }); @@ -531,17 +563,29 @@ const BlockUserAction = () => { LeadingIcon={BlockUserActionIcon} RootElement='button' rootProps={rootProps} - title={isBlocked ? t('Unblock') : t('Block user')} + title={ + isBlocked + ? t('common.unblock.ariaLabel', 'Unblock') + : t('channelDetail.channelManagementActions.blockUser.title', 'Block user') + } /> { onCancel={closeBlockUserAlert} onConfirm={isBlocked ? unblockUser : blockUser} testId='channel-detail-block-user-alert' - title={isBlocked ? t('Unblock') : t('Block User')} + title={ + isBlocked + ? t('common.unblock.ariaLabel', 'Unblock') + : t('common.blockUser.title', 'Block User') + } /> @@ -583,7 +631,7 @@ const LeaveChannelAction = () => { addNotification({ context: { channel }, emitter: 'ChannelManagementView', - message: t('Left channel'), + message: t('common.leftChannel.text', 'Left channel'), severity: 'success', type: 'api:channel:leave:success', }); @@ -594,7 +642,7 @@ const LeaveChannelAction = () => { context: { channel }, emitter: 'ChannelManagementView', error: toError(error), - message: t('Failed to leave channel'), + message: t('common.failedLeaveChannel.text', 'Failed to leave channel'), severity: 'error', type: 'api:channel:leave:failed', }); @@ -619,19 +667,28 @@ const LeaveChannelAction = () => { LeadingIcon={LeaveChannelActionIcon} RootElement='button' rootProps={rootProps} - title={t('Leave chat')} + title={t('channelDetail.channelManagementActions.leaveChat.title', 'Leave chat')} /> @@ -664,7 +721,10 @@ const DeleteChatAction = () => { addNotification({ context: { channel }, emitter: 'ChannelManagementView', - message: t('Chat deleted'), + message: t( + 'channelDetail.channelManagementActions.chatDeleted.text', + 'Chat deleted', + ), severity: 'success', type: 'api:channel:delete:success', }); @@ -675,7 +735,10 @@ const DeleteChatAction = () => { context: { channel }, emitter: 'ChannelManagementView', error: toError(error), - message: t('Error deleting chat'), + message: t( + 'channelDetail.channelManagementActions.errorDeletingChat.text', + 'Error deleting chat', + ), severity: 'error', type: 'api:channel:delete:failed', }); @@ -700,14 +763,21 @@ const DeleteChatAction = () => { LeadingIcon={DeleteChatActionIcon} RootElement='button' rootProps={rootProps} - title={t('Delete chat')} + title={t( + 'channelDetail.channelManagementActions.deleteChat.title', + 'Delete chat', + )} /> { onCancel={closeDeleteChatAlert} onConfirm={deleteChat} testId='channel-detail-delete-chat-alert' - title={t('Delete chat')} + title={t( + 'channelDetail.channelManagementActions.deleteChat.title', + 'Delete chat', + )} /> diff --git a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx index 429ebf83ab..7e593b70e6 100644 --- a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx @@ -174,7 +174,9 @@ const useChannelManagementEditForm = ({ const resolvedIsDmChannel = isDmChannel({ channel, ownUserId: client.user?.id }); const hasMembersOnline = useChannelHasMembersOnline({ channel }); const isOnline = resolvedIsDmChannel ? hasMembersOnline : undefined; - const nameLabel = resolvedIsDmChannel ? t('Contact name') : t('Group name'); + const nameLabel = resolvedIsDmChannel + ? t('channelDetail.channelManagementView.contactName.label', 'Contact name') + : t('channelDetail.channelManagementView.groupName.label', 'Group name'); // Dirty-tracking baseline; advanced to the saved value on success so the form // is no longer considered dirty (and the Save button hides) after a write. @@ -264,7 +266,10 @@ const useChannelManagementEditForm = ({ operation: 'update', status: 'success', }, - message: t('Changes saved'), + message: t( + 'channelDetail.channelManagementView.changesSaved.text', + 'Changes saved', + ), severity: 'success', }); } catch (error) { @@ -277,7 +282,10 @@ const useChannelManagementEditForm = ({ operation: 'update', status: 'failed', }, - message: t('Failed to save changes'), + message: t( + 'channelDetail.channelManagementView.failedSaveChanges.text', + 'Failed to save changes', + ), severity: 'error', }); } finally { @@ -360,7 +368,10 @@ export const ChannelManagementEditBody = (props: ChannelManagementEditBodyProps) type='button' variant='secondary' > - {t('Upload Picture')} + {t( + 'channelDetail.channelManagementView.uploadPicture.text', + 'Upload Picture', + )} {hasAvatarImage && ( )} - {t('Save')} + {t('channelDetail.channelManagementView.save.text', 'Save')} )} @@ -444,7 +455,10 @@ export const ChannelManagementView = ({ return ( ); }, @@ -461,17 +475,24 @@ export const ChannelManagementView = ({ const headerTitle = isEditMode ? resolvedIsDmChannel - ? t('Edit contact') - : t('Edit group') + ? t('channelDetail.channelManagementView.editContact.label', 'Edit contact') + : t('channelDetail.channelManagementView.editGroup.label', 'Edit group') : resolvedIsDmChannel - ? t('Contact info') - : t('Group info'); + ? t('channelDetail.channelManagementView.contactInfo.label', 'Contact info') + : t('channelDetail.channelManagementView.groupInfo.label', 'Group info'); return (
    setIsEditing(false) : undefined} title={headerTitle} TrailingContent={!isEditMode && canEditChannel ? EditChannelButton : undefined} diff --git a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsx b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsx index a03128e786..eb662725eb 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsx @@ -9,10 +9,16 @@ export const ChannelMediaEmptyList = () => {

    - {t('No photos or videos')} + {t( + 'channelDetail.channelMediaEmpty.noPhotosVideos.text', + 'No photos or videos', + )}

    - {t('Share a photo or video to see it here')} + {t( + 'channelDetail.channelMediaEmpty.sharePhotoVideoSee.text', + 'Share a photo or video to see it here', + )}

    diff --git a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx index 5e1257e958..e6d32b41c2 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx @@ -62,8 +62,16 @@ const ChannelMediaGridItem = ({ const durationLabel = formatTime(item.durationSeconds, 'floor'); const label = item.type === 'video' - ? t('aria/Open video shared by {{ name }}', { name: displayName }) - : t('aria/Open image shared by {{ name }}', { name: displayName }); + ? t( + 'channelDetail.channelMediaView.openVideoShared.ariaLabel', + 'Open video shared by {{ name }}', + { name: displayName }, + ) + : t( + 'channelDetail.channelMediaView.openImageShared.ariaLabel', + 'Open image shared by {{ name }}', + { name: displayName }, + ); return (
    @@ -273,7 +284,10 @@ export const ChannelMediaView: React.ComponentType = ({ return (
    - +
    diff --git a/src/plugins/ChannelDetail/Views/ChannelMediaView/__tests__/ChannelMediaView.test.tsx b/src/plugins/ChannelDetail/Views/ChannelMediaView/__tests__/ChannelMediaView.test.tsx index 2afdc62256..15cf994e97 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMediaView/__tests__/ChannelMediaView.test.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMediaView/__tests__/ChannelMediaView.test.tsx @@ -12,6 +12,7 @@ import { import { useStateStore } from '../../../../../store'; import { ChannelDetailProvider } from '../../../ChannelDetailContext'; import { ChannelMediaView } from '../ChannelMediaView'; +import { mockT } from '../../../../../mock-builders/translator'; const mocks = vi.hoisted(() => ({ searchSourceActivate: vi.fn(), @@ -126,7 +127,7 @@ describe('ChannelMediaView', () => { mocks.searchSourceOptions.length = 0; vi.mocked(useTranslationContext).mockReturnValue({ - t: (key: string) => key, + t: mockT, } as ReturnType); vi.mocked(useChatContext).mockReturnValue({ @@ -243,8 +244,8 @@ describe('ChannelMediaView', () => { renderView(); expect(getMediaItems()).toHaveLength(30); - expect(screen.queryByRole('button', { name: 'aria/Previous page' })).toBeNull(); - expect(screen.queryByRole('button', { name: 'aria/Next page' })).toBeNull(); + expect(screen.queryByRole('button', { name: 'Previous page' })).toBeNull(); + expect(screen.queryByRole('button', { name: 'Next page' })).toBeNull(); }); it('paginates 30 items per page through the previous/next buttons', () => { @@ -258,8 +259,8 @@ describe('ChannelMediaView', () => { // First page: 30 items, previous disabled, next enabled. expect(getMediaItems()).toHaveLength(30); - const previous = screen.getByRole('button', { name: 'aria/Previous page' }); - const next = screen.getByRole('button', { name: 'aria/Next page' }); + const previous = screen.getByRole('button', { name: 'Previous page' }); + const next = screen.getByRole('button', { name: 'Next page' }); expect(previous).toBeDisabled(); expect(next).toBeEnabled(); @@ -286,7 +287,7 @@ describe('ChannelMediaView', () => { renderView(); // First page is full and the source has more, so next stays enabled. - const next = screen.getByRole('button', { name: 'aria/Next page' }); + const next = screen.getByRole('button', { name: 'Next page' }); expect(next).toBeEnabled(); mocks.searchSourceSearch.mockClear(); @@ -343,12 +344,12 @@ describe('ChannelMediaView', () => { // First page shows 10 (not the default 30); 35 items spread across 4 pages. expect(getMediaItems()).toHaveLength(10); - expect(screen.getByRole('button', { name: 'aria/Previous page' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'aria/Next page' })).toBeEnabled(); + expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Next page' })).toBeEnabled(); - fireEvent.click(screen.getByRole('button', { name: 'aria/Next page' })); + fireEvent.click(screen.getByRole('button', { name: 'Next page' })); expect(getMediaItems()).toHaveLength(10); - expect(screen.getByRole('button', { name: 'aria/Previous page' })).toBeEnabled(); + expect(screen.getByRole('button', { name: 'Previous page' })).toBeEnabled(); }); }); diff --git a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx index b959f6f380..3ba8b14b20 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx @@ -238,7 +238,10 @@ const SendDirectMessageAction = () => { context: { channel }, emitter: 'ChannelMemberDetail', error: toError(error), - message: t('Error opening direct message'), + message: t( + 'channelDetail.channelMemberActions.errorOpeningDirectMessage.text', + 'Error opening direct message', + ), severity: 'error', type: 'api:channel:watch:failed', }); @@ -271,7 +274,10 @@ const SendDirectMessageAction = () => { LeadingIcon={SendDirectMessageActionIcon} RootElement='button' rootProps={rootProps} - title={t('Send direct message')} + title={t( + 'channelDetail.channelMemberActions.sendDirectMessage.title', + 'Send direct message', + )} /> ); }; @@ -301,7 +307,10 @@ const UserMuteAction = () => { addNotification({ context: { channel }, emitter: 'ChannelMemberDetail', - message: t('User unmuted'), + message: t( + 'channelDetail.channelManagementActions.userUnmuted.text', + 'User unmuted', + ), severity: 'success', type: 'api:user:unmute:success', }), @@ -315,7 +324,10 @@ const UserMuteAction = () => { context: { channel }, emitter: 'ChannelMemberDetail', error: toError(error), - message: t('Error unmuting user'), + message: t( + 'channelDetail.channelManagementActions.errorUnmutingUser.text', + 'Error unmuting user', + ), severity: 'error', type: 'api:user:unmute:failed', }); @@ -328,7 +340,10 @@ const UserMuteAction = () => { addNotification({ context: { channel }, emitter: 'ChannelMemberDetail', - message: t('User muted'), + message: t( + 'channelDetail.channelManagementActions.userMuted.text', + 'User muted', + ), severity: 'success', type: 'api:user:mute:success', }), @@ -339,7 +354,10 @@ const UserMuteAction = () => { context: { channel }, emitter: 'ChannelMemberDetail', error: toError(error), - message: t('Error muting user'), + message: t( + 'channelDetail.channelManagementActions.errorMutingUser.text', + 'Error muting user', + ), severity: 'error', type: 'api:user:mute:failed', }); @@ -386,7 +404,11 @@ const UserMuteAction = () => { LeadingIcon={optimisticUserMuted ? MemberUnmuteActionIcon : MemberMuteActionIcon} RootElement='button' rootProps={rootProps} - title={optimisticUserMuted ? t('Unmute user') : t('Mute user')} + title={ + optimisticUserMuted + ? t('channelDetail.channelManagementActions.unmuteUser.title', 'Unmute user') + : t('channelDetail.channelManagementActions.muteUser.title', 'Mute user') + } TrailingSlot={TrailingSlot} /> ); @@ -424,7 +446,7 @@ const BlockUserAction = () => { addNotification({ context: { channel }, emitter: 'ChannelMemberDetail', - message: t('User unblocked'), + message: t('common.userUnblocked.text', 'User unblocked'), severity: 'success', type: 'api:user:unblock:success', }); @@ -433,7 +455,10 @@ const BlockUserAction = () => { context: { channel }, emitter: 'ChannelMemberDetail', error: toError(error), - message: t('Error unblocking user'), + message: t( + 'channelDetail.channelManagementActions.errorUnblockingUser.text', + 'Error unblocking user', + ), severity: 'error', type: 'api:user:unblock:failed', }); @@ -452,7 +477,7 @@ const BlockUserAction = () => { addNotification({ context: { channel }, emitter: 'ChannelMemberDetail', - message: t('User blocked'), + message: t('common.userBlocked.text', 'User blocked'), severity: 'success', type: 'api:user:block:success', }); @@ -461,7 +486,10 @@ const BlockUserAction = () => { context: { channel }, emitter: 'ChannelMemberDetail', error: toError(error), - message: t('Error blocking user'), + message: t( + 'channelDetail.channelManagementActions.errorBlockingUser.text', + 'Error blocking user', + ), severity: 'error', type: 'api:user:block:failed', }); @@ -487,27 +515,47 @@ const BlockUserAction = () => { LeadingIcon={BlockUserActionIcon} RootElement='button' rootProps={rootProps} - title={isBlocked ? t('Unblock user') : t('Block user')} + title={ + isBlocked + ? t('channelDetail.channelMemberActions.unblockUser.title', 'Unblock user') + : t('channelDetail.channelManagementActions.blockUser.title', 'Block user') + } /> @@ -540,7 +588,7 @@ const RemoveUserAction = () => { addNotification({ context: { channel }, emitter: 'ChannelMemberDetail', - message: t('User removed'), + message: t('channelDetail.channelMemberActions.userRemoved.text', 'User removed'), severity: 'success', type: 'api:channel:remove-members:success', }); @@ -550,7 +598,10 @@ const RemoveUserAction = () => { context: { channel }, emitter: 'ChannelMemberDetail', error: toError(error), - message: t('Error removing user'), + message: t( + 'channelDetail.channelMemberActions.errorRemovingUser.text', + 'Error removing user', + ), severity: 'error', type: 'api:channel:remove-members:failed', }); @@ -575,21 +626,28 @@ const RemoveUserAction = () => { LeadingIcon={RemoveUserActionIcon} RootElement='button' rootProps={rootProps} - title={t('Remove user')} + title={t('channelDetail.channelMemberActions.removeUser.title', 'Remove user')} /> diff --git a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberDetail.tsx b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberDetail.tsx index 73ae17297d..a0a98562c3 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberDetail.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberDetail.tsx @@ -33,17 +33,21 @@ const getPresenceStatusText = ( user: ChannelMemberResponse['user'], t: ReturnType['t'], ) => { - if (user?.online) return t('Online'); + if (user?.online) return t('common.online.label', 'Online'); if (user?.last_active) { - return t('Last seen {{ timestamp }}', { - timestamp: t('timestamp/ChannelMembersLastActive', { - timestamp: user.last_active, - }), - }); + return t( + 'channelDetail.channelMemberDetail.lastSeen.label', + 'Last seen {{ timestamp }}', + { + timestamp: t('timestamp.ChannelMembersLastActive', { + timestamp: user.last_active, + }), + }, + ); } - return t('Offline'); + return t('common.offline.label', 'Offline'); }; export const ChannelMemberDetail = ({ @@ -99,7 +103,11 @@ const ChannelMemberDetailContent = ({ return (
    - +
    { vi.clearAllMocks(); vi.mocked(useTranslationContext).mockReturnValue({ - t: (key: string, options?: { timestamp?: string }) => - options?.timestamp ? `${key}:${options.timestamp}` : key, + t: mockT, } as ReturnType); vi.mocked(useChatContext).mockReturnValue({ diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx index 88e4f26c89..5454ee4d0a 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx @@ -112,7 +112,11 @@ const ChannelMembersAddViewItem = ({ @@ -207,7 +211,11 @@ export const ChannelMembersAddView = ({ () => function ChannelMembersAddEmptyPlaceholder() { if (isLoading || !users) return null; - return {t('No user found')}; + return ( + + {t('channelDetail.channelMembersAdd.noUserFound.text', 'No user found')} + + ); }, [isLoading, t, users], ); @@ -229,7 +237,11 @@ export const ChannelMembersAddView = ({ addNotification({ context: { channel }, emitter: 'ChannelMembersView', - message: t('{{ count }} members added', { count: selectedUserIds.length }), + message: t('channelDetail.channelMembersAdd.membersAdded.text', { + count: selectedUserIds.length, + defaultValue_one: '{{ count }} member added', + defaultValue_other: '{{ count }} members added', + }), severity: 'success', type: 'api:channel:addMembers:success', }); @@ -242,7 +254,10 @@ export const ChannelMembersAddView = ({ context: { channel }, emitter: 'ChannelMembersView', error: error as Error, - message: t('Error adding members'), + message: t( + 'channelDetail.channelMembersAdd.errorAddingMembers.text', + 'Error adding members', + ), severity: 'error', type: 'api:channel:addMembers:failed', }); @@ -267,7 +282,11 @@ export const ChannelMembersAddView = ({ - {t('Add {{ count }} members', { count: selectedUserIds.length })} + {t('channelDetail.channelMembersAdd.addMembers.text', { + count: selectedUserIds.length, + defaultValue_one: 'Add {{ count }} member', + defaultValue_other: 'Add {{ count }} members', + })} diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx index 477bffc0a4..429da89c06 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx @@ -22,10 +22,12 @@ const getMemberRoleTranslation = ( member: ChannelMemberResponse, t: ReturnType['t'], ) => { - if ([member.user?.role, member.channel_role].includes('admin')) return t('Admin'); + if ([member.user?.role, member.channel_role].includes('admin')) + return t('channelDetail.channelMembersBrowse.admin.label', 'Admin'); if (member.channel_role === 'channel_moderator' || member.channel_role === 'moderator') - return t('Moderator'); - if (member.role === 'owner') return t('Owner'); + return t('channelDetail.channelMembersBrowse.moderator.label', 'Moderator'); + if (member.role === 'owner') + return t('channelDetail.channelMembersBrowse.owner.label', 'Owner'); return undefined; }; @@ -34,17 +36,21 @@ const getPresenceStatusText = ( user: ChannelMemberResponse['user'], t: ReturnType['t'], ) => { - if (user?.online) return t('Online'); + if (user?.online) return t('common.online.label', 'Online'); if (user?.last_active) { - return t('Last seen {{ timestamp }}', { - timestamp: t('timestamp/ChannelMembersLastActive', { - timestamp: user.last_active, - }), - }); + return t( + 'channelDetail.channelMemberDetail.lastSeen.label', + 'Last seen {{ timestamp }}', + { + timestamp: t('timestamp.ChannelMembersLastActive', { + timestamp: user.last_active, + }), + }, + ); } - return t('Offline'); + return t('common.offline.label', 'Offline'); }; const ChannelMembersBrowseViewItem = ({ @@ -94,9 +100,13 @@ const ChannelMembersBrowseViewItem = ({ const rootProps = useMemo( () => ({ - 'aria-label': t('View member details for {{ member }}', { - member: displayName, - }), + 'aria-label': t( + 'channelDetail.channelMembersBrowse.viewMemberDetails.ariaLabel', + 'View member details for {{ member }}', + { + member: displayName, + }, + ), className: 'str-chat__channel-detail__channel-members-view__list-item', onClick: () => onMemberSelect?.(member), }), @@ -160,7 +170,14 @@ export const ChannelMembersBrowseView = ({ const EmptyPlaceholder = useMemo( () => function ChannelMembersEmptyPlaceholder() { - return {t('No member found')}; + return ( + + {t( + 'channelDetail.channelMembersBrowse.noMemberFound.text', + 'No member found', + )} + + ); }, [t], ); diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx index e125f3c5d0..dec4cdd36a 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx @@ -75,13 +75,16 @@ const AddMembersHeaderAction = ({ return ( ); }; @@ -96,14 +99,17 @@ const AddMembersMenuAction = ({ return ( { modeController.setMode('add'); closeMenu?.(); }} > - {t('Add')} + {t('channelDetail.channelMembersHeader.add.text', 'Add')} ); }; @@ -136,14 +142,17 @@ export const DefaultHeaderActionsMenuTrigger = ({ return ( ); }; diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.tsx index 0c80c9f694..5f4bb6e162 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.tsx @@ -65,7 +65,7 @@ const isReservedMode = (mode: ChannelMembersViewMode) => RESERVED_MODES.includes const AddMembersModeTitle = () => { const { t } = useTranslationContext(); - return <>{t('Add members')}; + return <>{t('channelDetail.channelMembersView.addMembers.label', 'Add members')}; }; /** Built-in mode descriptors. Merged with (and overridable by) the `modeViews` prop. */ @@ -159,13 +159,24 @@ export const ChannelMembersView = ({
    ) : ( - t('{{ count }} members', { count: memberCount }) + t('channelDetail.channelMembersView.members.title', { + count: memberCount, + defaultValue_one: '{{ count }} member', + defaultValue_other: '{{ count }} members', + }) ) } TrailingContent={HeaderTrailingActions} diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsx index 630656a724..3fb6a7e34c 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsx @@ -17,6 +17,7 @@ import { querySelectableMemberButton, renderWithChannel, } from './testUtils'; +import { mockT } from '../../../../../mock-builders/translator'; const mocks = vi.hoisted(() => ({ virtuosoRenderCount: 0, @@ -88,8 +89,7 @@ describe('ChannelMembersAddView', () => { mocks.virtuosoRenderCount = 0; vi.mocked(useTranslationContext).mockReturnValue({ - t: (key: string, options?: { count?: number }) => - options?.count ? `${key}:${options.count}` : key, + t: mockT, } as ReturnType); vi.mocked(useChatContext).mockReturnValue({ @@ -148,11 +148,9 @@ describe('ChannelMembersAddView', () => { fireEvent.click(getSelectableMemberButton('Bob')); fireEvent.click(getSelectableMemberButton('Carol')); - expect( - screen.getByRole('button', { name: 'Add {{ count }} members:2' }), - ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add 2 members' })).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: 'Add {{ count }} members:2' })); + fireEvent.click(screen.getByRole('button', { name: 'Add 2 members' })); await waitFor(() => { expect(channel.addMembers).toHaveBeenCalledWith(['user-2', 'user-3']); @@ -204,16 +202,14 @@ describe('ChannelMembersAddView', () => { ); fireEvent.click(getSelectableMemberButton('Bob')); - expect( - screen.getByRole('button', { name: /Add {{ count }} members/ }), - ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Add \d+ members?/ })).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: /Add {{ count }} members/ })); + fireEvent.click(screen.getByRole('button', { name: /Add \d+ members?/ })); await waitFor(() => expect(channel.addMembers).toHaveBeenCalledWith(['user-2'])); expect( - screen.queryByRole('button', { name: /Add {{ count }} members/ }), + screen.queryByRole('button', { name: /Add \d+ members?/ }), ).not.toBeInTheDocument(); }); @@ -235,7 +231,7 @@ describe('ChannelMembersAddView', () => { document.querySelector('.str-chat__channel-detail__channel-members-view__checkbox'), ).not.toBeInTheDocument(); expect( - screen.queryByRole('button', { name: /Add {{ count }} members/ }), + screen.queryByRole('button', { name: /Add \d+ members?/ }), ).not.toBeInTheDocument(); }); diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsx index 2c6cf0afde..b20b98d36a 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsx @@ -10,6 +10,7 @@ import { import { useStateStore } from '../../../../../store'; import { ChannelMembersBrowseView } from '../ChannelMembersBrowseView'; import { createChannel, emitChannelEvent, renderWithChannel } from './testUtils'; +import { mockT } from '../../../../../mock-builders/translator'; const mocks = vi.hoisted(() => ({ searchSourceActivate: vi.fn(), @@ -115,11 +116,7 @@ describe('ChannelMembersBrowseView', () => { mocks.searchSourceOptions.length = 0; vi.mocked(useTranslationContext).mockReturnValue({ - t: (key: string, options?: { count?: number; timestamp?: string }) => { - if (options?.count) return `${key}:${options.count}`; - if (options?.timestamp) return `${key}:${options.timestamp}`; - return key; - }, + t: mockT, } as ReturnType); vi.mocked(useChatContext).mockReturnValue({ mutes: [], diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx index e9fb24cbd2..7819ed114c 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx @@ -19,6 +19,7 @@ import type { ChannelMembersModeController, ChannelMembersViewMode, } from '../ChannelMembersView'; +import { mockT } from '../../../../../mock-builders/translator'; vi.mock('../../../../../context'); @@ -75,7 +76,7 @@ describe('ChannelMembersHeaderActions.defaults', () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(useTranslationContext).mockReturnValue({ - t: (key: string) => key, + t: mockT, } as ReturnType); vi.mocked(useModalContext).mockReturnValue({} as ReturnType); vi.mocked(useComponentContext).mockReturnValue( diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.test.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.test.tsx index 29d170ef5e..f9fdbcfa89 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.test.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.test.tsx @@ -14,6 +14,7 @@ import { import type { ChannelMembersHeaderActionItem } from '../ChannelMembersHeaderActions.defaults'; import { useChannelMemberCount } from '../useChannelMemberCount'; import { createChannel, renderWithChannel } from './testUtils'; +import { mockT } from '../../../../../mock-builders/translator'; vi.mock('../useChannelMemberCount'); @@ -209,8 +210,7 @@ describe('ChannelMembersView', () => { vi.clearAllMocks(); vi.mocked(useTranslationContext).mockReturnValue({ - t: (key: string, options?: { count?: number }) => - options?.count ? `${key}:${options.count}` : key, + t: mockT, } as ReturnType); vi.mocked(useModalContext).mockReturnValue({ @@ -347,9 +347,7 @@ describe('ChannelMembersView', () => { fireEvent.click(screen.getByRole('button', { name: 'Go back' })); expect(screen.getByTestId('channel-members-browse-view')).toBeInTheDocument(); - expect( - screen.getByRole('heading', { name: '{{ count }} members:2' }), - ).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '2 members' })).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'Remove channel members' }), ).toBeInTheDocument(); diff --git a/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsx b/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsx index 210c9610a2..54ca916f12 100644 --- a/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsx +++ b/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsx @@ -9,10 +9,16 @@ export const PinnedMessagesEmptyList = () => {

    - {t('No pinned messages')} + {t( + 'channelDetail.pinnedMessagesEmpty.noPinnedMessages.text', + 'No pinned messages', + )}

    - {t('Pin a message to see it here')} + {t( + 'channelDetail.pinnedMessagesEmpty.pinMessageSee.text', + 'Pin a message to see it here', + )}

    diff --git a/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesView.tsx b/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesView.tsx index bdf9ef302a..a1babb1cf7 100644 --- a/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesView.tsx +++ b/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesView.tsx @@ -45,7 +45,10 @@ const getPinnedMessagePreview = ( const attachmentPreview = attachment?.title || attachment?.text || attachment?.fallback || attachment?.type; - return attachmentPreview || t('Pinned message'); + return ( + attachmentPreview || + t('channelDetail.pinnedMessagesView.pinnedMessage.label', 'Pinned message') + ); }; const PinnedMessageDate = ({ message }: { message: PinnedMessage }) => { @@ -58,7 +61,7 @@ const PinnedMessageDate = ({ message }: { message: PinnedMessage }) => { messageCreatedAt: normalizedTimestamp, t, tDateTimeParser, - timestampTranslationKey: 'timestamp/ChannelDetailPinnedMessageTimestamp', + timestampTranslationKey: 'timestamp.ChannelDetailPinnedMessageTimestamp', }), [normalizedTimestamp, t, tDateTimeParser], ); @@ -171,7 +174,12 @@ export const PinnedMessagesView: React.ComponentType = if (!hasPinnedMessages) return ; if (hasSearchResultsLoaded) return ( - {t('No messages found')} + + {t( + 'channelDetail.pinnedMessagesView.noMessagesFound.text', + 'No messages found', + )} + ); return null; }, @@ -192,8 +200,14 @@ export const PinnedMessagesView: React.ComponentType =
    {hasPinnedMessages && ( diff --git a/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx b/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx index 20a437fc35..4b4e8ef579 100644 --- a/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx +++ b/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx @@ -18,6 +18,7 @@ import { useModalContext, useTranslationContext, } from '../../../../../context'; +import { mockT } from '../../../../../mock-builders/translator'; import { useStateStore } from '../../../../../store'; import { ChannelDetailProvider } from '../../../ChannelDetailContext'; import { PinnedMessagesView } from '../PinnedMessagesView'; @@ -222,11 +223,14 @@ describe('PinnedMessagesView', () => { mocks.searchSourceOptions.length = 0; vi.mocked(useTranslationContext).mockReturnValue({ - t: (key: string, options?: { timestamp?: Date }) => { - if (key === 'timestamp/ChannelDetailPinnedMessageTimestamp') { + t: (key: string, second?: unknown, third?: unknown) => { + const options = (typeof second === 'object' ? second : third) as + | { timestamp?: Date } + | undefined; + if (key === 'timestamp.ChannelDetailPinnedMessageTimestamp') { return options?.timestamp?.toISOString() ?? key; } - return key; + return mockT(key, second as never, third as never); }, tDateTimeParser: (input?: string | Date) => new Date(input ?? Date.now()), } as ReturnType); diff --git a/src/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx b/src/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx index 17e75dc177..1563a99ce9 100644 --- a/src/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx +++ b/src/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx @@ -10,7 +10,6 @@ import { defaultChannelManagementActionSet, useBaseChannelManagementActionSetFilter, } from '../Views/ChannelManagementView/ChannelManagementActions.defaults'; - const mocks = vi.hoisted(() => { const addNotification = vi.fn(); const blockUser = vi.fn(); @@ -19,7 +18,26 @@ const mocks = vi.hoisted(() => { const mute = vi.fn(); const muteUser = vi.fn(); const removeMembers = vi.fn(); - const t = vi.fn((key: string) => key); + // Inlined rather than imported: `vi.hoisted` runs before module imports are initialized. + const t = vi.fn((key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }); const unblockUser = vi.fn(); const unmute = vi.fn(); const unmuteUser = vi.fn(); @@ -112,7 +130,11 @@ vi.mock('../../../context', () => ({ }), useModalContext: () => ({ close: mocks.close }), useTranslationContext: () => ({ - t: mocks.useStableTranslationFunction ? mocks.t : (key: string) => mocks.t(key), + // The unstable variant must still forward the inline defaultValue, or every call would + // resolve to the raw key. + t: mocks.useStableTranslationFunction + ? mocks.t + : (...args: unknown[]) => mocks.t(...args), }), })); diff --git a/src/plugins/ChannelDetail/__tests__/ChannelManagementView.test.tsx b/src/plugins/ChannelDetail/__tests__/ChannelManagementView.test.tsx index e877d8df1a..b34305c390 100644 --- a/src/plugins/ChannelDetail/__tests__/ChannelManagementView.test.tsx +++ b/src/plugins/ChannelDetail/__tests__/ChannelManagementView.test.tsx @@ -40,7 +40,27 @@ vi.mock('../../../context', () => ({ Avatar: () =>
    , }), useModalContext: () => ({ close: mocks.close }), - useTranslationContext: () => ({ t: (key: string) => key }), + useTranslationContext: () => ({ + t: (key: string, second?: unknown, third?: unknown) => { + const defaultValue = typeof second === 'string' ? second : undefined; + const options = ((typeof second === 'object' ? second : third) ?? {}) as Record< + string, + unknown + >; + let template = defaultValue; + if (template === undefined && typeof options.count === 'number') { + template = ( + options.count === 1 ? options.defaultValue_one : options.defaultValue_other + ) as string | undefined; + } + template ??= options.defaultValue as string | undefined; + template ??= key; + return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => { + const value = options[name]; + return value === undefined || value === null ? whole : String(value); + }); + }, + }), })); vi.mock('../../../context/ChatContext', () => ({ diff --git a/src/plugins/Emojis/EmojiPicker.tsx b/src/plugins/Emojis/EmojiPicker.tsx index 4972da647d..758e00a36f 100644 --- a/src/plugins/Emojis/EmojiPicker.tsx +++ b/src/plugins/Emojis/EmojiPicker.tsx @@ -124,7 +124,7 @@ export const EmojiPicker = (props: EmojiPickerProps) => { ); }; @@ -41,14 +45,17 @@ export const RemoveMembersMenuAction = ({ return ( { modeController.setMode('remove'); closeMenu?.(); }} > - {t('Remove')} + {t(asDynamicKey('viteExample.removeMembers.trigger.label'), 'Remove')} ); }; diff --git a/examples/vite/src/ChatLayout/ChannelMembersRemoveView.tsx b/examples/vite/src/ChatLayout/ChannelMembersRemoveView.tsx index 7ff64c16ef..d19d785930 100644 --- a/examples/vite/src/ChatLayout/ChannelMembersRemoveView.tsx +++ b/examples/vite/src/ChatLayout/ChannelMembersRemoveView.tsx @@ -1,6 +1,7 @@ import type { ChannelMemberResponse, UserResponse } from 'stream-chat'; import { useMemo, useState } from 'react'; import { + asDynamicKey, Avatar, Checkbox, InfiniteScrollPaginator, @@ -38,17 +39,21 @@ const getPresenceStatusText = ( user: ChannelMemberResponse['user'], t: ReturnType['t'], ) => { - if (user?.online) return t('Online'); + if (user?.online) return t('common.online.label', 'Online'); if (user?.last_active) { - return t('Last seen {{ timestamp }}', { - timestamp: t('timestamp/ChannelMembersLastActive', { - timestamp: user.last_active, - }), - }); + return t( + 'channelDetail.channelMemberDetail.lastSeen.label', + 'Last seen {{ timestamp }}', + { + timestamp: t('timestamp.ChannelMembersLastActive', { + timestamp: user.last_active, + }), + }, + ); } - return t('Offline'); + return t('common.offline.label', 'Offline'); }; export const ChannelMembersRemoveView = ({ @@ -89,7 +94,11 @@ export const ChannelMembersRemoveView = ({ addNotification({ context: { channel }, emitter: 'ChannelMembersRemoveView', - message: t('Removed {{ count }} members', { count: memberCount }), + message: t(asDynamicKey('viteExample.removeMembers.removed.text'), { + count: memberCount, + defaultValue_one: 'Removed {{ count }} member', + defaultValue_other: 'Removed {{ count }} members', + }), severity: 'success', type: 'api:channel:remove-members:success', }); @@ -101,7 +110,10 @@ export const ChannelMembersRemoveView = ({ context: { channel }, emitter: 'ChannelMembersRemoveView', error: toError(error), - message: t('Error removing members'), + message: t( + asDynamicKey('viteExample.removeMembers.error.text'), + 'Error removing members', + ), severity: 'error', type: 'api:channel:remove-members:failed', }); @@ -155,7 +167,12 @@ export const ChannelMembersRemoveView = ({ ); }) ) : ( - {t('No member found')} + + {t( + 'channelDetail.channelMembersBrowse.noMemberFound.text', + 'No member found', + )} + )} @@ -167,7 +184,11 @@ export const ChannelMembersRemoveView = ({ disabled={isRemoving} onClick={handleRemove} > - {t('Remove {{ count }} members', { count: selectedMemberUserIds.length })} + {t(asDynamicKey('viteExample.removeMembers.submit.label'), { + count: selectedMemberUserIds.length, + defaultValue_one: 'Remove {{ count }} member', + defaultValue_other: 'Remove {{ count }} members', + })} diff --git a/examples/vite/src/ChatLayout/ConfiguredChannelDetail.tsx b/examples/vite/src/ChatLayout/ConfiguredChannelDetail.tsx index 90d37afb15..6f3fc6d9d9 100644 --- a/examples/vite/src/ChatLayout/ConfiguredChannelDetail.tsx +++ b/examples/vite/src/ChatLayout/ConfiguredChannelDetail.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { useTranslationContext } from 'stream-chat-react'; +import { asDynamicKey, useTranslationContext } from 'stream-chat-react'; import { AvatarWithChannelDetail, type AvatarWithChannelDetailProps, @@ -18,7 +18,11 @@ import { ChannelMembersRemoveView } from './ChannelMembersRemoveView'; const ChannelMembersRemoveTitle = () => { const { t } = useTranslationContext(); - return <>{t('Manage members')}; + return ( + <> + {t(asDynamicKey('viteExample.channelDetail.manageMembers.title'), 'Manage members')} + + ); }; // Register the app-provided bulk-remove mode. The SDK ships no bulk removal; the diff --git a/examples/vite/src/ChatLayout/Panels.tsx b/examples/vite/src/ChatLayout/Panels.tsx index aaa5721cae..5d11f1da51 100644 --- a/examples/vite/src/ChatLayout/Panels.tsx +++ b/examples/vite/src/ChatLayout/Panels.tsx @@ -338,7 +338,7 @@ const LayerThreadHeader = ({ thread }: ThreadHeaderProps) => {
    -
    {t('Thread')}
    +
    + {t('thread.header.thread.text', 'Thread')} +
    - {replyCount === 1 - ? t('1 reply') - : t('{{ count }} replies', { count: replyCount })} + {/* One plural key rather than a hand-rolled count branch: i18next picks the form via + Intl.PluralRules, so a language with more categories works without touching this. */} + {t('common.replyCount.label', { + count: replyCount, + defaultValue_one: '1 reply', + defaultValue_other: '{{ count }} replies', + })}
    diff --git a/examples/vite/src/CustomMessageActions/ConfigurableMessageActions.tsx b/examples/vite/src/CustomMessageActions/ConfigurableMessageActions.tsx index ee3a11ee5e..4093b07e1a 100644 --- a/examples/vite/src/CustomMessageActions/ConfigurableMessageActions.tsx +++ b/examples/vite/src/CustomMessageActions/ConfigurableMessageActions.tsx @@ -9,6 +9,7 @@ import { import type { DeleteMessageOptions, LocalMessage } from 'stream-chat'; import { Alert, + asDynamicKey, Button, ContextMenuButton, defaultMessageActionSet, @@ -71,8 +72,14 @@ const CustomDeleteMessageAlert = ({ data-testid='message-delete-alert' > {enableOptionConfiguration && (
    @@ -81,7 +88,10 @@ const CustomDeleteMessageAlert = ({ id='delete-message-alert-delete-only-for-me-switch' onChange={(event) => setDeleteForMe(event.target.checked)} > - {t('Delete for me only')} + {t( + asDynamicKey('viteExample.deleteAlert.deleteForMeOnly.label'), + 'Delete for me only', + )} - {t('Hard delete')} + {t(asDynamicKey('viteExample.deleteAlert.hardDelete.label'), 'Hard delete')} - {t('Soft delete')} + {t(asDynamicKey('viteExample.deleteAlert.softDelete.label'), 'Soft delete')}
    )} @@ -120,7 +130,7 @@ const CustomDeleteMessageAlert = ({ size='md' variant='danger' > - {t('Delete message')} + {t('messageActions.deleteMessageAlert.deleteMessage.title', 'Delete message')} @@ -174,7 +184,7 @@ const CustomDeleteMessageAction = () => { return ( { @@ -183,7 +193,7 @@ const CustomDeleteMessageAction = () => { }} variant='destructive' > - {t('Delete message')} + {t('messageActions.deleteMessageAlert.deleteMessage.title', 'Delete message')} ); }; @@ -198,7 +208,7 @@ const CustomMarkOwnUnreadMessageAction = () => { return ( { @@ -209,7 +219,10 @@ const CustomMarkOwnUnreadMessageAction = () => { message, }, emitter: 'MessageActions', - message: t('Message marked as unread'), + message: t( + 'messageActions.messageMarkedUnread.text', + 'Message marked as unread', + ), severity: 'success', type: 'api:message:markUnread:success', }); @@ -223,6 +236,7 @@ const CustomMarkOwnUnreadMessageAction = () => { message: getErrorMessage( error, t( + 'messageActions.errorMarkingMessageUnread.text', 'Error marking message unread. Cannot mark unread messages older than the newest 100 channel messages.', ), ), @@ -234,7 +248,7 @@ const CustomMarkOwnUnreadMessageAction = () => { } }} > - {t('Mark as unread')} + {t('messageActions.markUnread.text', 'Mark as unread')} ); }; @@ -465,7 +479,7 @@ export const ConfigurableMessageActions = ( message: deleteDialogTarget.message, }, emitter: 'MessageActions', - message: t('Message deleted'), + message: t('common.messageDeleted.text', 'Message deleted'), severity: 'success', type: 'api:message:delete:success', }); @@ -476,7 +490,10 @@ export const ConfigurableMessageActions = ( }, emitter: 'MessageActions', error: getNotificationError(error), - message: getErrorMessage(error, t('Error deleting message')), + message: getErrorMessage( + error, + t('common.errorDeletingMessage.label', 'Error deleting message'), + ), severity: 'error', type: 'api:message:delete:failed', }); diff --git a/examples/vite/src/Sidebar/SidebarToggle.tsx b/examples/vite/src/Sidebar/SidebarToggle.tsx index 1b0e7ed507..95371d04cd 100644 --- a/examples/vite/src/Sidebar/SidebarToggle.tsx +++ b/examples/vite/src/Sidebar/SidebarToggle.tsx @@ -1,6 +1,11 @@ import { useState } from 'react'; import { useSidebar } from '../ChatLayout/SidebarContext.tsx'; -import { Button, PopperTooltip, useTranslationContext } from 'stream-chat-react'; +import { + asDynamicKey, + Button, + PopperTooltip, + useTranslationContext, +} from 'stream-chat-react'; import { IconSidebar } from '../icons.tsx'; export const SidebarToggle = () => { @@ -10,11 +15,15 @@ export const SidebarToggle = () => { const [tooltipVisible, setTooltipVisible] = useState(false); const tooltipText = sidebarOpen ? 'Close sidebar' : 'Open sidebar'; + const toggleLabel = sidebarOpen + ? t(asDynamicKey('viteExample.sidebar.collapse.ariaLabel'), 'Collapse sidebar') + : t(asDynamicKey('viteExample.sidebar.expand.ariaLabel'), 'Expand sidebar'); + return ( <> + ))} +
    +
    Text direction
    diff --git a/examples/vite/src/i18n/de.ts b/examples/vite/src/i18n/de.ts new file mode 100644 index 0000000000..660cae7a4e --- /dev/null +++ b/examples/vite/src/i18n/de.ts @@ -0,0 +1,811 @@ +// German for the example app — the annotated one of the two languages here. See ./index.ts for +// how these exports are registered, and ./it.ts for the same shape without the commentary. +// +// This is a *complete* dictionary: every key the SDK defines is translated, and the type assertion +// below fails the build if a future SDK release adds one that is missing here. Partial dictionaries +// are equally valid — an unsupplied key renders the English copy that ships inline at its call site, +// never a raw `some.dotted.key` — so start small and grow if you prefer. +// +// The side-effect import is what localizes month and weekday names. +import 'dayjs/locale/de.js'; + +import type { TranslationCatalog, TranslationDictionary } from 'stream-chat-react'; + +export const deTranslations = { + 'a11y.accessibleLabel.active.ariaLabel': 'Aktiv', + 'a11y.accessibleLabel.unreadMessage.ariaLabel_one': '{{ count }} ungelesene Nachricht', + 'a11y.accessibleLabel.unreadMessage.ariaLabel_other': + '{{ count }} ungelesene Nachrichten', + 'a11y.incomingMessageAnnouncements.newMessage.label': 'Neue Nachricht von {{user}}', + 'a11y.interactionAnnouncements.commandActivated.ariaLabel': + 'Befehl aktiviert: {{ command }}', + 'a11y.interactionAnnouncements.droppedPosition.ariaLabel': + '„{{ option }}“ an Position {{ position }} abgelegt.', + 'a11y.interactionAnnouncements.giphyCanceled.ariaLabel': 'Giphy abgebrochen', + 'a11y.interactionAnnouncements.giphyImageChanged.ariaLabel': 'Giphy-Bild geändert', + 'a11y.interactionAnnouncements.giphyImageChanged.withTitle.ariaLabel': + 'Giphy-Bild geändert: {{ title }}', + 'a11y.interactionAnnouncements.giphySent.ariaLabel': 'Giphy gesendet', + 'a11y.interactionAnnouncements.noSearchResultsFound.ariaLabel': + 'Keine Suchergebnisse gefunden', + 'a11y.interactionAnnouncements.openedChannel.ariaLabel': 'Kanal geöffnet: {{ name }}', + 'a11y.interactionAnnouncements.openedThread.ariaLabel': 'Thread in {{ name }} geöffnet', + 'a11y.interactionAnnouncements.pickedUpUseArrow.ariaLabel': + '„{{ option }}“ aufgenommen. Mit den Pfeiltasten neu anordnen. Leertaste oder Tab zum Ablegen.', + 'a11y.interactionAnnouncements.pollDialogOpened.ariaLabel': 'Umfragedialog geöffnet', + 'a11y.interactionAnnouncements.pollSent.ariaLabel': 'Umfrage gesendet', + 'a11y.interactionAnnouncements.pressEnterStartTyping.ariaLabel': + 'Eingabetaste drücken, um zu schreiben', + 'a11y.interactionAnnouncements.recordingPaused.ariaLabel': 'Aufnahme pausiert', + 'a11y.interactionAnnouncements.recordingResumed.ariaLabel': 'Aufnahme fortgesetzt', + 'a11y.interactionAnnouncements.recordingStarted.ariaLabel': 'Aufnahme gestartet', + 'a11y.interactionAnnouncements.removedOption.ariaLabel': 'Option {{ option }} entfernt', + 'a11y.interactionAnnouncements.searchCleared.ariaLabel': 'Suche geleert', + 'a11y.interactionAnnouncements.searchResults.ariaLabel_one': '{{ count }} Suchergebnis', + 'a11y.interactionAnnouncements.searchResults.ariaLabel_other': + '{{ count }} Suchergebnisse', + 'a11y.interactionAnnouncements.suggestions.ariaLabel_one': '{{ count }} Vorschlag', + 'a11y.interactionAnnouncements.suggestions.ariaLabel_other': '{{ count }} Vorschläge', + 'a11y.interactionAnnouncements.suggestionsWithLabel.ariaLabel_one': + '{{ count }} {{ suggestionsLabel }}', + 'a11y.interactionAnnouncements.suggestionsWithLabel.ariaLabel_other': + '{{ count }} {{ suggestionsLabel }}', + 'a11y.interactionAnnouncements.userSelected.ariaLabel': + 'Benutzer ausgewählt: {{ user }}', + 'a11y.interactionAnnouncements.voiceMessageSent.ariaLabel': 'Sprachnachricht gesendet', + 'a11y.interactionAnnouncements.voiceRecordingAttached.ariaLabel': + 'Sprachaufnahme angehängt', + 'aiState.indicator.generating.label': 'Wird erstellt...', + 'aiState.indicator.thinking.label': 'Denkt nach...', + 'attachment.actions.giphyActions.ariaLabel': 'Giphy-Aktionen', + 'attachment.actions.giphyPreviewOnlyVisible.ariaLabel': + 'Giphy-Vorschau, nur für dich sichtbar. Nutze die Aktionen Senden, Mischen oder Abbrechen.', + 'attachment.actions.shuffle.label': 'Mischen', + 'attachment.geolocation.liveUntil.text': 'Live bis {{ timestamp }}', + 'attachment.geolocation.locationSharingEnded.text': 'Standortfreigabe beendet', + 'attachment.geolocation.openLocationMap.ariaLabel': 'Standort auf einer Karte öffnen', + 'attachment.geolocation.stopSharing.text': 'Freigabe beenden', + 'attachment.giphy.animatedGif.ariaLabel': 'Animiertes GIF', + 'attachment.giphy.animatedGif.withTitle.ariaLabel': 'Animiertes GIF: {{ title }}', + 'attachment.modalGallery.openGalleryImage.label': 'Galerie bei Bild {{ index }} öffnen', + 'attachment.modalGallery.openImageGallery.label': 'Bild in der Galerie öffnen', + 'attachment.unableRenderCard.text': 'dieser Inhalt konnte nicht angezeigt werden', + 'attachment.visibilityDisclaimer.onlyVisible.text': 'Nur für dich sichtbar', + 'audioPlayback.audioPlayerNotifications.cannotSeekRecording.label': + 'In der Aufnahme kann nicht gesucht werden', + 'audioPlayback.audioPlayerNotifications.failedPlayRecording.label': + 'Aufnahme konnte nicht abgespielt werden', + 'audioPlayback.audioPlayerNotifications.recordingFormatNotSupported.label': + 'Das Aufnahmeformat wird nicht unterstützt und kann nicht abgespielt werden', + 'audioPlayback.progressBar.seekAudioPosition.ariaLabel': 'Audioposition suchen', + 'audioPlayback.progressBarA11y.audioPosition.ariaLabel': + 'Audioposition {{ elapsed }} von {{ duration }}', + 'audioPlayback.progressBarA11y.audioPositionPercent.ariaLabel': + 'Audioposition {{ progress }} Prozent', + 'baseImage.imagePlaceholder.imageFailedLoad.ariaLabel': + 'Bild konnte nicht geladen werden', + 'channel.channelMissing.text': 'Kanal fehlt', + 'channelDetail.avatarChannelDetail.channelDetails.ariaLabel': 'Kanaldetails', + 'channelDetail.avatarChannelDetail.openChannelDetails.ariaLabel': 'Kanaldetails öffnen', + 'channelDetail.channelFilesEmpty.noFiles.text': 'Keine Dateien', + 'channelDetail.channelFilesEmpty.shareFileSee.text': + 'Teile eine Datei, um sie hier zu sehen', + 'channelDetail.channelFilesView.files.title': 'Dateien', + 'channelDetail.channelManagementActions.blockUser.title': 'Benutzer blockieren', + 'channelDetail.channelManagementActions.chatDeleted.text': 'Chat gelöscht', + 'channelDetail.channelManagementActions.deleteChat.title': 'Chat löschen', + 'channelDetail.channelManagementActions.errorBlockingUser.text': + 'Fehler beim Blockieren des Benutzers', + 'channelDetail.channelManagementActions.errorDeletingChat.text': + 'Fehler beim Löschen des Chats', + 'channelDetail.channelManagementActions.errorMutingChannel.text': + 'Fehler beim Stummschalten des Kanals', + 'channelDetail.channelManagementActions.errorMutingUser.text': + 'Fehler beim Stummschalten des Benutzers', + 'channelDetail.channelManagementActions.errorUnblockingUser.text': + 'Fehler beim Aufheben der Blockierung', + 'channelDetail.channelManagementActions.errorUnmutingChannel.text': + 'Fehler beim Aufheben der Stummschaltung des Kanals', + 'channelDetail.channelManagementActions.errorUnmutingUser.text': + 'Fehler beim Aufheben der Stummschaltung des Benutzers', + 'channelDetail.channelManagementActions.leaveChat.title': 'Chat verlassen', + 'channelDetail.channelManagementActions.muteChat.title': 'Chat stummschalten', + 'channelDetail.channelManagementActions.muteUser.title': 'Benutzer stummschalten', + 'channelDetail.channelManagementActions.permanentlyDeletesMessageHistory.description': + 'Dies löscht deinen Nachrichtenverlauf mit {{ user }} endgültig. Das kann nicht rückgängig gemacht werden.', + 'channelDetail.channelManagementActions.sureWantLeaveChannel.description': + 'Möchtest du diesen Kanal wirklich verlassen?', + 'channelDetail.channelManagementActions.unmuteChat.title': + 'Stummschaltung des Chats aufheben', + 'channelDetail.channelManagementActions.unmuteUser.title': + 'Stummschaltung des Benutzers aufheben', + 'channelDetail.channelManagementActions.userAbleMessageAgain.description': + 'Dieser Benutzer kann dir wieder schreiben.', + 'channelDetail.channelManagementActions.userMuted.text': 'Benutzer stummgeschaltet', + 'channelDetail.channelManagementActions.userUnmuted.text': + 'Stummschaltung des Benutzers aufgehoben', + 'channelDetail.channelManagementActions.userWonTAble.description': + 'Dieser Benutzer kann dir nicht mehr schreiben. Du kannst die Blockierung jederzeit aufheben.', + 'channelDetail.channelManagementView.changesSaved.text': 'Änderungen gespeichert', + 'channelDetail.channelManagementView.contactInfo.label': 'Kontaktinfo', + 'channelDetail.channelManagementView.contactName.label': 'Kontaktname', + 'channelDetail.channelManagementView.edit.text': 'Bearbeiten', + 'channelDetail.channelManagementView.editChatData.ariaLabel': 'Chatdaten bearbeiten', + 'channelDetail.channelManagementView.editContact.label': 'Kontakt bearbeiten', + 'channelDetail.channelManagementView.editGroup.label': 'Gruppe bearbeiten', + 'channelDetail.channelManagementView.failedSaveChanges.text': + 'Änderungen konnten nicht gespeichert werden', + 'channelDetail.channelManagementView.groupInfo.label': 'Gruppeninfo', + 'channelDetail.channelManagementView.groupName.label': 'Gruppenname', + 'channelDetail.channelManagementView.manageChannel.description': 'Kanal verwalten', + 'channelDetail.channelManagementView.save.text': 'Speichern', + 'channelDetail.channelManagementView.uploadPicture.text': 'Bild hochladen', + 'channelDetail.channelMediaEmpty.noPhotosVideos.text': 'Keine Fotos oder Videos', + 'channelDetail.channelMediaEmpty.sharePhotoVideoSee.text': + 'Teile ein Foto oder Video, um es hier zu sehen', + 'channelDetail.channelMediaView.next.text': 'Weiter', + 'channelDetail.channelMediaView.nextPage.ariaLabel': 'Nächste Seite', + 'channelDetail.channelMediaView.openImageShared.ariaLabel': + 'Von {{ name }} geteiltes Bild öffnen', + 'channelDetail.channelMediaView.openVideoShared.ariaLabel': + 'Von {{ name }} geteiltes Video öffnen', + 'channelDetail.channelMediaView.photosVideos.title': 'Fotos & Videos', + 'channelDetail.channelMediaView.previous.text': 'Zurück', + 'channelDetail.channelMediaView.previousPage.ariaLabel': 'Vorherige Seite', + 'channelDetail.channelMemberActions.ableMessageAgain.description': + '{{ member }} kann dir wieder schreiben.', + 'channelDetail.channelMemberActions.errorOpeningDirectMessage.text': + 'Fehler beim Öffnen der Direktnachricht', + 'channelDetail.channelMemberActions.errorRemovingUser.text': + 'Fehler beim Entfernen des Benutzers', + 'channelDetail.channelMemberActions.removeChannel.description': + '{{ member }} aus diesem Kanal entfernen?', + 'channelDetail.channelMemberActions.removeUser.title': 'Benutzer entfernen', + 'channelDetail.channelMemberActions.sendDirectMessage.title': 'Direktnachricht senden', + 'channelDetail.channelMemberActions.unblockUser.title': 'Blockierung aufheben', + 'channelDetail.channelMemberActions.userRemoved.text': 'Benutzer entfernt', + 'channelDetail.channelMemberActions.wonTAbleMessage.description': + '{{ member }} kann dir nicht mehr schreiben.', + 'channelDetail.channelMemberDetail.lastSeen.label': 'Zuletzt gesehen {{ timestamp }}', + 'channelDetail.channelMemberDetail.memberDetail.title': 'Mitgliedsdetails', + 'channelDetail.channelMembersAdd.addMembers.text_one': + '{{ count }} Mitglied hinzufügen', + 'channelDetail.channelMembersAdd.addMembers.text_other': + '{{ count }} Mitglieder hinzufügen', + 'channelDetail.channelMembersAdd.alreadyMember.label': 'Bereits Mitglied', + 'channelDetail.channelMembersAdd.errorAddingMembers.text': + 'Fehler beim Hinzufügen von Mitgliedern', + 'channelDetail.channelMembersAdd.membersAdded.text_one': + '{{ count }} Mitglied hinzugefügt', + 'channelDetail.channelMembersAdd.membersAdded.text_other': + '{{ count }} Mitglieder hinzugefügt', + 'channelDetail.channelMembersAdd.noUserFound.text': 'Kein Benutzer gefunden', + 'channelDetail.channelMembersBrowse.admin.label': 'Administrator', + 'channelDetail.channelMembersBrowse.moderator.label': 'Moderator', + 'channelDetail.channelMembersBrowse.noMemberFound.text': 'Kein Mitglied gefunden', + 'channelDetail.channelMembersBrowse.owner.label': 'Eigentümer', + 'channelDetail.channelMembersBrowse.viewMemberDetails.ariaLabel': + 'Mitgliedsdetails für {{ member }} ansehen', + 'channelDetail.channelMembersHeader.actions.text': 'Aktionen', + 'channelDetail.channelMembersHeader.add.text': 'Hinzufügen', + 'channelDetail.channelMembersHeader.addChannelMembers.ariaLabel': + 'Kanalmitglieder hinzufügen', + 'channelDetail.channelMembersHeader.openMembersActions.ariaLabel': + 'Mitgliederaktionen öffnen', + 'channelDetail.channelMembersView.addMembers.label': 'Mitglieder hinzufügen', + 'channelDetail.channelMembersView.browseChannelMembers.description': + 'Kanalmitglieder durchsuchen', + 'channelDetail.channelMembersView.members.title_one': '{{ count }} Mitglied', + 'channelDetail.channelMembersView.members.title_other': '{{ count }} Mitglieder', + 'channelDetail.pinnedMessagesEmpty.noPinnedMessages.text': + 'Keine angepinnten Nachrichten', + 'channelDetail.pinnedMessagesEmpty.pinMessageSee.text': + 'Pinne eine Nachricht an, um sie hier zu sehen', + 'channelDetail.pinnedMessagesView.browsePinnedMessages.description': + 'Angepinnte Nachrichten durchsuchen', + 'channelDetail.pinnedMessagesView.noMessagesFound.text': 'Keine Nachrichten gefunden', + 'channelDetail.pinnedMessagesView.pinnedMessage.label': 'Angepinnte Nachricht', + 'channelDetail.pinnedMessagesView.pinnedMessages.title': 'Angepinnte Nachrichten', + 'channelDetail.sectionNavigatorHeader.openMenu.ariaLabel': 'Menü öffnen', + 'channelHeader.online.members.label': '{{ memberCount }} Mitglieder', + 'channelHeader.online.online.label': '{{ watcherCount }} online', + 'channelList.channelList.ariaLabel': 'Kanalliste', + 'channelList.header.chats.text': 'Chats', + 'channelListItem.archive.title': 'Archivieren', + 'channelListItem.attachment.ariaLabel': 'Anhang', + 'channelListItem.attachment.text': '🏙 Anhang...', + 'channelListItem.attachment.withAttachmentType.ariaLabel': + 'Anhang {{ attachmentType }}', + 'channelListItem.attachmentCount.ariaLabel_one': '{{ count }} Anhang', + 'channelListItem.attachmentCount.ariaLabel_other': '{{ count }} Anhänge', + 'channelListItem.audio.ariaLabel': 'Audio', + 'channelListItem.channelActions.ariaLabel': 'Kanalaktionen', + 'channelListItem.channelArchived.text': 'Kanal archiviert', + 'channelListItem.channelDisplayName.directMessage.label': 'Direktnachricht', + 'channelListItem.channelPinned.text': 'Kanal angepinnt', + 'channelListItem.channelUnarchived.text': 'Kanal aus dem Archiv geholt', + 'channelListItem.channelUnpinned.text': 'Kanal losgelöst', + 'channelListItem.created.text': '📊 {{createdBy}} hat erstellt: {{ pollName}}', + 'channelListItem.delivered.ariaLabel': 'Zugestellt', + 'channelListItem.deliveryStatus.ariaLabel': 'Zustellstatus: {{ deliveryStatus }}', + 'channelListItem.failedBlockUser.text': 'Benutzer konnte nicht blockiert werden', + 'channelListItem.failedUpdateChannelArchive.text': + 'Archivstatus des Kanals konnte nicht aktualisiert werden', + 'channelListItem.failedUpdateChannelMute.text': + 'Stummschaltung des Kanals konnte nicht aktualisiert werden', + 'channelListItem.failedUpdateChannelPinned.text': + 'Pinn-Status des Kanals konnte nicht aktualisiert werden', + 'channelListItem.file.ariaLabel': 'Datei', + 'channelListItem.gif.ariaLabel': 'GIF', + 'channelListItem.image.ariaLabel': 'Bild', + 'channelListItem.lastMessage.withMessagePreview.ariaLabel': + 'Letzte Nachricht: {{ messagePreview }}', + 'channelListItem.lastMessage.withSenderAndMessagePreview.ariaLabel': + 'Letzte Nachricht von {{ sender }}: {{ messagePreview }}', + 'channelListItem.leaveChannel.title': 'Kanal verlassen', + 'channelListItem.messageAttachments.ariaLabel': 'Nachricht mit Anhängen', + 'channelListItem.noMessagesChat.ariaLabel': 'In diesem Chat gibt es keine Nachrichten.', + 'channelListItem.openChannelActionsMenu.ariaLabel': 'Menü mit Kanalaktionen öffnen', + 'channelListItem.poll.ariaLabel': 'Umfrage: {{ pollName }}', + 'channelListItem.read.ariaLabel': 'Gelesen', + 'channelListItem.sent.ariaLabel': 'Gesendet', + 'channelListItem.sharedLink.ariaLabel': 'Hat einen Link geteilt', + 'channelListItem.sharedLinkTitle.ariaLabel': + 'Hat einen Link geteilt mit dem Titel: {{ linkTitle }}', + 'channelListItem.sharedLocation.ariaLabel': 'Standort geteilt', + 'channelListItem.sharedLocation.text': '📍Standort geteilt', + 'channelListItem.unarchive.title': 'Aus Archiv holen', + 'channelListItem.unblockUser.title': 'Blockierung aufheben', + 'channelListItem.video.ariaLabel': 'Video', + 'channelListItem.voiceMessage.ariaLabel': 'Sprachnachricht', + 'channelListItem.voted.text': '📊 {{votedBy}} hat abgestimmt: {{pollOptionText}}', + 'chat.reportLostConnection.waitingNetwork.text': 'Warte auf Netzwerk…', + 'command.ban.args': '[@benutzername] [text]', + 'command.ban.description': 'Einen Benutzer sperren', + 'command.giphy.args': '[text]', + 'command.giphy.description': 'Ein zufälliges GIF im Kanal posten', + 'command.mute.args': '[@benutzername]', + 'command.mute.description': 'Einen Benutzer stummschalten', + 'command.unban.args': '[@benutzername]', + 'command.unban.description': 'Sperre eines Benutzers aufheben', + 'command.unmute.args': '[@benutzername]', + 'command.unmute.description': 'Stummschaltung eines Benutzers aufheben', + 'common.addReaction.text': 'Reaktion hinzufügen', + 'common.anonymous.label': 'Anonym', + 'common.back.label': 'Zurück', + 'common.blockUser.title': 'Benutzer blockieren', + 'common.cancel.label': 'Abbrechen', + 'common.channelMuted.text': 'Kanal stummgeschaltet', + 'common.channelUnmuted.text': 'Kanal nicht mehr stummgeschaltet', + 'common.close.ariaLabel': 'Schließen', + 'common.createQuestionAddOptions.label': + 'Frage erstellen, Optionen hinzufügen und Umfrageeinstellungen festlegen', + 'common.currentLocation.text': 'Aktueller Standort', + 'common.delete.text': 'Löschen', + 'common.downloadAttachment.ariaLabel': 'Anhang herunterladen', + 'common.downloadAttachment.title': 'Anhang herunterladen', + 'common.editMessage.text': 'Nachricht bearbeiten', + 'common.emptyMessage.text': 'Leere Nachricht...', + 'common.errorDeletingMessage.label': 'Fehler beim Löschen der Nachricht', + 'common.errorMutingUser.label': 'Fehler beim Stummschalten eines Benutzers ...', + 'common.errorPinningMessage.label': 'Fehler beim Anpinnen der Nachricht', + 'common.errorRemovingMessagePin.label': 'Fehler beim Entfernen der Pinnnadel', + 'common.errorUnmutingUser.label': 'Fehler beim Aufheben der Stummschaltung ...', + 'common.failedLeaveChannel.text': 'Kanal konnte nicht verlassen werden', + 'common.lastActivity.ariaLabel': 'Letzte Aktivität: {{ time }}', + 'common.leftChannel.text': 'Kanal verlassen', + 'common.liveLocation.text': 'Live-Standort', + 'common.location.text': 'Standort', + 'common.messageDeleted.text': 'Nachricht gelöscht', + 'common.messagePinned.label': 'Nachricht angepinnt', + 'common.mute.title': 'Stummschalten', + 'common.muted.label': '{{ user }} wurde stummgeschaltet', + 'common.newMessages.label_one': '{{count}} neue Nachricht', + 'common.newMessages.label_other': '{{count}} neue Nachrichten', + 'common.nothingYet.text': 'Noch nichts...', + 'common.offline.label': 'Offline', + 'common.online.label': 'Online', + 'common.openReactionSelector.ariaLabel': 'Reaktionsauswahl öffnen', + 'common.pause.ariaLabel': 'Pause', + 'common.pin.title': 'Anpinnen', + 'common.play.ariaLabel': 'Abspielen', + 'common.playbackSpeedX.label': 'Wiedergabegeschwindigkeit {{ rate }}x', + 'common.poll.label': 'Umfrage', + 'common.reminderSet.text': 'Erinnerung gesetzt', + 'common.replyCount.label_one': '1 Antwort', + 'common.replyCount.label_other': '{{ count }} Antworten', + 'common.resultsLoaded.label': 'Alle Ergebnisse geladen', + 'common.retryUpload.ariaLabel': 'Upload wiederholen', + 'common.savedLater.text': 'Für später gespeichert', + 'common.search.ariaLabel': 'Suchen', + 'common.send.label': 'Senden', + 'common.threads.text': 'Threads', + 'common.unblock.ariaLabel': 'Blockierung aufheben', + 'common.unmute.title': 'Stummschaltung aufheben', + 'common.unmuted.label': 'Die Stummschaltung von {{ user }} wurde aufgehoben', + 'common.unpin.title': 'Loslösen', + 'common.unsupportedAttachment.text': 'Nicht unterstützter Anhang', + 'common.userBlocked.text': 'Benutzer blockiert', + 'common.userUnblocked.text': 'Blockierung des Benutzers aufgehoben', + 'common.userUploadedContent.label': 'Von Benutzern hochgeladene Inhalte', + 'common.voiceMessage.label': 'Sprachnachricht', + 'common.you.label': 'Du', + 'dialog.callout.closeCalloutDialog.ariaLabel': 'Hinweisdialog schließen', + 'dialog.contextMenu.backParentMenuButton.ariaLabel': 'Zurück zum übergeordneten Menü', + 'dialog.contextMenu.submenu.ariaLabel': 'Untermenü', + 'dialog.prompt.goBack.ariaLabel': 'Zurück', + 'dialog.viewer.closeDialog.ariaLabel': 'Dialog schließen', + 'emojiPicker.emojiPicker.ariaLabel': 'Emoji-Auswahl', + 'emptyState.indicator.noConversationsYet.label': 'Noch keine Unterhaltungen', + 'emptyState.indicator.noItemsExist.text': 'Keine Einträge vorhanden', + 'emptyState.indicator.startConversation.label': + 'Schreibe eine Nachricht, um die Unterhaltung zu beginnen', + 'fileUpload.uploadButton.fileUpload.ariaLabel': 'Datei-Upload', + 'form.numericInput.decreaseValue.ariaLabel': 'Wert verringern', + 'form.numericInput.increaseValue.ariaLabel': 'Wert erhöhen', + 'form.switchField.disabled.ariaLabel': '{{ setting }} deaktiviert', + 'form.switchField.enabled.ariaLabel': '{{ setting }} aktiviert', + 'gallery.ui.nextImage.ariaLabel': 'Nächstes Bild', + 'gallery.ui.previousImage.ariaLabel': 'Vorheriges Bild', + 'language.af': 'Afrikaans', + 'language.am': 'Amharisch', + 'language.ar': 'Arabisch', + 'language.az': 'Aserbaidschanisch', + 'language.bg': 'Bulgarisch', + 'language.bn': 'Bengalisch', + 'language.bs': 'Bosnisch', + 'language.cs': 'Tschechisch', + 'language.da': 'Dänisch', + 'language.de': 'Deutsch', + 'language.el': 'Griechisch', + 'language.en': 'Englisch', + 'language.es': 'Spanisch', + 'language.es-MX': 'Spanisch (Mexiko)', + 'language.et': 'Estnisch', + 'language.fa': 'Persisch', + 'language.fa-AF': 'Dari', + 'language.fi': 'Finnisch', + 'language.fr': 'Französisch', + 'language.fr-CA': 'Französisch (Kanada)', + 'language.ha': 'Hausa', + 'language.he': 'Hebräisch', + 'language.hi': 'Hindi', + 'language.hr': 'Kroatisch', + 'language.ht': 'Haitianisch', + 'language.hu': 'Ungarisch', + 'language.id': 'Indonesisch', + 'language.it': 'Italienisch', + 'language.ja': 'Japanisch', + 'language.ka': 'Georgisch', + 'language.ko': 'Koreanisch', + 'language.lt': 'Litauisch', + 'language.lv': 'Lettisch', + 'language.ms': 'Malaiisch', + 'language.nl': 'Niederländisch', + 'language.no': 'Norwegisch', + 'language.pl': 'Polnisch', + 'language.ps': 'Paschtu', + 'language.pt': 'Portugiesisch', + 'language.ro': 'Rumänisch', + 'language.ru': 'Russisch', + 'language.sk': 'Slowakisch', + 'language.sl': 'Slowenisch', + 'language.so': 'Somali', + 'language.sq': 'Albanisch', + 'language.sr': 'Serbisch', + 'language.sv': 'Schwedisch', + 'language.sw': 'Swahili', + 'language.ta': 'Tamil', + 'language.th': 'Thai', + 'language.tl': 'Tagalog', + 'language.tr': 'Türkisch', + 'language.uk': 'Ukrainisch', + 'language.ur': 'Urdu', + 'language.vi': 'Vietnamesisch', + 'language.zh': 'Chinesisch (vereinfacht)', + 'language.zh-TW': 'Chinesisch (traditionell)', + 'loadMore.button.loadMore.label': 'Mehr laden', + 'loading.errorIndicator.error.text': 'Fehler: {{ errorMessage }}', + 'loading.progressIndicators.percentComplete.ariaLabel': + '{{percent}} Prozent abgeschlossen', + 'location.shareLocationDialog.attach.text': 'Anhängen', + 'location.shareLocationDialog.description': + 'Wähle deinen aktuellen Standort und aktiviere optional die Live-Standortfreigabe', + 'location.shareLocationDialog.share.text': 'Teilen', + 'location.shareLocationDialog.shareLiveLocation.title': 'Live-Standort teilen für', + 'location.shareLocationDialog.shareLocation.title': 'Standort teilen', + 'mediaRecorder.audioRecorderRecording.cancelRecording.ariaLabel': 'Aufnahme abbrechen', + 'mediaRecorder.audioRecorderRecording.completeRecording.ariaLabel': + 'Aufnahme abschließen', + 'mediaRecorder.audioRecorderRecording.pauseRecording.ariaLabel': 'Aufnahme pausieren', + 'mediaRecorder.audioRecorderRecording.resumeRecording.ariaLabel': 'Aufnahme fortsetzen', + 'mediaRecorder.audioRecorderRecording.voiceMessageDeleted.text': + 'Sprachnachricht gelöscht', + 'mediaRecorder.audioRecordingButton.startRecordingAudio.ariaLabel': + 'Audioaufnahme starten', + 'mediaRecorder.error.processing': + 'Bei der Verarbeitung der Aufnahme ist ein Fehler aufgetreten', + 'mediaRecorder.error.recording': 'Bei der Aufnahme ist ein Fehler aufgetreten', + 'mediaRecorder.error.start': 'Fehler beim Starten der Aufnahme', + 'mediaRecorder.permissionDenied.camera.body': + 'Erlaube den Kamerazugriff in deinem Browser, um die Aufnahme zu starten', + 'mediaRecorder.permissionDenied.camera.heading': 'Zugriff auf die Kamera erlauben', + 'mediaRecorder.permissionDenied.microphone.body': + 'Erlaube den Mikrofonzugriff in deinem Browser, um die Aufnahme zu starten', + 'mediaRecorder.permissionDenied.microphone.heading': + 'Zugriff auf das Mikrofon erlauben', + 'mention.channel.description': 'Alle in diesem Kanal benachrichtigen', + 'mention.here.description': 'Alle online Mitglieder in diesem Kanal benachrichtigen', + 'message.alsoSent.alsoSentChannel.text': 'Auch im Kanal gesendet', + 'message.alsoSent.repliedThread.text': 'Hat auf einen Thread geantwortet', + 'message.alsoSent.view.text': 'Ansehen', + 'message.and.withCommaSeparatedUsersAndLastUser.label': + '{{ commaSeparatedUsers }} und {{ lastUser }}', + 'message.and.withFirstUserAndSecondUser.label': '{{ firstUser }} und {{ secondUser }}', + 'message.blocked.text': 'Die Nachricht wurde durch Moderationsrichtlinien blockiert', + 'message.editedIndicator.edited.text': 'Bearbeitet', + 'message.more.label': '{{ commaSeparatedUsers }} und {{ moreCount }} weitere', + 'message.pinIndicator.pinned.label': 'Von dir angepinnt', + 'message.pinIndicator.pinned.withName.label': 'Von {{ name }} angepinnt', + 'message.reminderNotification.due.label': 'Fällig {{ timeLeft }}', + 'message.reminderNotification.dueSince.label': 'Fällig seit {{ dueSince }}', + 'message.status.delivered.text': 'Zugestellt', + 'message.status.sending.text': 'Wird gesendet...', + 'message.status.sent.text': 'Gesendet', + 'message.text.message.ariaLabel': 'Nachricht,', + 'message.text.message.withUser.ariaLabel': 'Nachricht von {{ user }},', + 'message.translationIndicator.original.text': 'Original', + 'message.translationIndicator.translated.text': 'Übersetzt', + 'message.translationIndicator.translated.withLanguage.text': + 'Übersetzt aus {{ language }}', + 'message.translationIndicator.viewOriginal.text': 'Original ansehen', + 'message.translationIndicator.viewTranslation.text': 'Übersetzung ansehen', + 'message.ui.reviewBouncedMessage.ariaLabel': 'Abgelehnte Nachricht prüfen', + 'messageActions.blockUser.ariaLabel': 'Benutzer blockieren', + 'messageActions.bookmarkMessage.ariaLabel': 'Nachricht merken', + 'messageActions.copyMessage.text': 'Nachricht kopieren', + 'messageActions.copyMessageText.ariaLabel': 'Nachrichtentext kopieren', + 'messageActions.deleteMessage.ariaLabel': 'Nachricht löschen', + 'messageActions.deleteMessageAlert.deleteMessage.title': 'Nachricht löschen', + 'messageActions.deleteMessageAlert.description': + 'Möchtest du diese Nachricht wirklich löschen?', + 'messageActions.downloadSubmenu.download.label': '{{ fileName }} herunterladen', + 'messageActions.downloadSubmenu.download.text': 'Alle herunterladen', + 'messageActions.downloadSubmenu.downloadAttachment.label': + 'Anhang {{ number }} herunterladen', + 'messageActions.editMessage.ariaLabel': 'Nachricht bearbeiten', + 'messageActions.errorAddingFlag.text': 'Fehler beim Melden', + 'messageActions.errorMarkingMessageUnread.text': + 'Fehler beim Markieren als ungelesen. Nachrichten, die älter als die neuesten 100 Kanalnachrichten sind, können nicht als ungelesen markiert werden.', + 'messageActions.flag.text': 'Melden', + 'messageActions.flagMessage.ariaLabel': 'Nachricht melden', + 'messageActions.markMessageUnread.ariaLabel': 'Nachricht als ungelesen markieren', + 'messageActions.markUnread.text': 'Als ungelesen markieren', + 'messageActions.messageActions.ariaLabel': 'Nachrichtenaktionen', + 'messageActions.messageMarkedUnread.text': 'Nachricht als ungelesen markiert', + 'messageActions.messageSuccessfullyFlagged.text': + 'Die Nachricht wurde erfolgreich gemeldet', + 'messageActions.messageUnpinned.text': 'Nachricht losgelöst', + 'messageActions.muteUser.ariaLabel': 'Benutzer stummschalten', + 'messageActions.openMessageActionsMenu.ariaLabel': + 'Menü mit Nachrichtenaktionen öffnen', + 'messageActions.openThread.ariaLabel': 'Thread öffnen', + 'messageActions.pinMessage.ariaLabel': 'Nachricht anpinnen', + 'messageActions.quoteMessage.ariaLabel': 'Nachricht zitieren', + 'messageActions.quoteReply.text': 'Zitiert antworten', + 'messageActions.remindMe.text': 'Erinnere mich', + 'messageActions.remindMeMessage.ariaLabel': 'An Nachricht erinnern', + 'messageActions.remindMeSubmenu.remindMe.text': 'Erinnere mich', + 'messageActions.removeReminder.ariaLabel': 'Erinnerung entfernen', + 'messageActions.removeReminder.text': 'Erinnerung entfernen', + 'messageActions.removeSaveLater.ariaLabel': 'Aus „Für später gespeichert“ entfernen', + 'messageActions.removeSaveLater.text': 'Aus „Für später gespeichert“ entfernen', + 'messageActions.resend.text': 'Erneut senden', + 'messageActions.resendMessage.ariaLabel': 'Nachricht erneut senden', + 'messageActions.saveLater.text': 'Für später speichern', + 'messageActions.threadReply.text': 'Im Thread antworten', + 'messageActions.unmuteUser.ariaLabel': 'Stummschaltung des Benutzers aufheben', + 'messageActions.unpinMessage.ariaLabel': 'Nachricht loslösen', + 'messageBounce.prompt.description': + 'Prüfe diese Nachricht und entscheide, ob du sie löschen, bearbeiten oder trotzdem senden möchtest', + 'messageBounce.prompt.sendAnyway.text': 'Trotzdem senden', + 'messageBounce.prompt.title': + 'Diese Nachricht entspricht nicht unseren Inhaltsrichtlinien', + 'messageComposer.attachmentPreviewRoot.showPreview.ariaLabel': 'Vorschau anzeigen', + 'messageComposer.attachmentSelector.attachmentActions.ariaLabel': 'Anhangsaktionen', + 'messageComposer.attachmentSelector.commands.text': 'Befehle', + 'messageComposer.attachmentSelector.file.text': 'Datei', + 'messageComposer.attachmentSelector.openAttachmentSelector.ariaLabel': + 'Anhangsauswahl öffnen', + 'messageComposer.audioAttachmentPreview.fileTooLarge.text': 'Datei zu groß', + 'messageComposer.audioAttachmentPreview.retryUpload.text': 'Upload wiederholen', + 'messageComposer.audioAttachmentPreview.uploadBlocked.text': 'Upload blockiert', + 'messageComposer.audioAttachmentPreview.uploadError.text': 'Upload-Fehler', + 'messageComposer.audioAttachmentPreview.uploadFailed.text': 'Upload fehlgeschlagen', + 'messageComposer.commandChip.exitCommand.ariaLabel': 'Befehl {{ command }} verlassen', + 'messageComposer.commandsMenu.backAttachments.ariaLabel': 'Zurück zu den Anhängen', + 'messageComposer.commandsMenu.instantCommands.text': 'Sofortbefehle', + 'messageComposer.dragDropUpload.dragFiles.text': 'Ziehe deine Dateien hierher', + 'messageComposer.dragDropUpload.someFilesNotAccepted.text': + 'Einige der Dateien werden nicht akzeptiert', + 'messageComposer.geolocationPreview.live.text': 'Live für {{duration}}', + 'messageComposer.geolocationPreview.location.text': 'Standort: {{ coordinates }}', + 'messageComposer.geolocationPreview.removeLocationAttachment.ariaLabel': + 'Standortanhang entfernen', + 'messageComposer.geolocationPreview.sharedLocation.title': 'Geteilter Standort', + 'messageComposer.icons.attachFiles.text': 'Dateien anhängen', + 'messageComposer.quotedMessagePreview.cancelReply.ariaLabel': 'Antwort abbrechen', + 'messageComposer.quotedMessagePreview.files.label_one': '{{ count }} Datei', + 'messageComposer.quotedMessagePreview.files.label_other': '{{ count }} Dateien', + 'messageComposer.quotedMessagePreview.jumpQuotedMessage.ariaLabel': + 'Zur zitierten Nachricht springen', + 'messageComposer.quotedMessagePreview.photo.label': 'Foto', + 'messageComposer.quotedMessagePreview.photos.label_one': '{{ count }} Foto', + 'messageComposer.quotedMessagePreview.photos.label_other': '{{ count }} Fotos', + 'messageComposer.quotedMessagePreview.reply.text': 'Antworten', + 'messageComposer.quotedMessagePreview.reply.withAuthorName.text': + 'Antwort an {{ authorName }}', + 'messageComposer.quotedMessagePreview.video.label': 'Video', + 'messageComposer.quotedMessagePreview.videos.label_one': '{{ count }} Video', + 'messageComposer.quotedMessagePreview.videos.label_other': '{{ count }} Videos', + 'messageComposer.quotedMessagePreview.voiceMessage.label': + 'Sprachnachricht {{ duration }}', + 'messageComposer.removeAttachmentPreview.removeAttachment.ariaLabel': + 'Anhang entfernen', + 'messageComposer.sendButton.send.ariaLabel': 'Senden', + 'messageComposer.sendChannelCheckbox.alsoSendChannel.label': 'Auch im Kanal senden', + 'messageComposer.sendChannelCheckbox.alsoSendDirectMessage.label': + 'Auch als Direktnachricht senden', + 'messageComposer.sendMessageFn.sendMessageRequestFailed.text': + 'Senden der Nachricht fehlgeschlagen', + 'messageComposer.stopAiGeneration.stopAiGeneration.ariaLabel': 'KI-Generierung stoppen', + 'messageComposer.updateMessageFn.editMessageRequestFailed.text': + 'Bearbeiten der Nachricht fehlgeschlagen', + 'messageList.newMessageNotification.newMessages.label': 'Neue Nachrichten!', + 'messageList.scrollLatestMessage.jumpLatestMessage.ariaLabel': + 'Zur neuesten Nachricht springen', + 'messageList.unreadMessagesNotification.markMessagesRead.ariaLabel': + 'Nachrichten als gelesen markieren', + 'messageList.unreadMessagesNotification.unread.text_one': '{{count}} ungelesen', + 'messageList.unreadMessagesNotification.unread.text_other': '{{count}} ungelesen', + 'messageList.unreadMessagesNotification.unreadMessages.text': 'Ungelesene Nachrichten', + 'messagePreview.latestMessagePreview.fileCount.label_one': 'Datei', + 'messagePreview.latestMessagePreview.fileCount.label_other': '{{ count }} Dateien', + 'messagePreview.latestMessagePreview.imageCount.label_one': 'Bild', + 'messagePreview.latestMessagePreview.imageCount.label_other': '{{ count }} Bilder', + 'messagePreview.latestMessagePreview.linkCount.label_one': 'Link', + 'messagePreview.latestMessagePreview.linkCount.label_other': '{{ count }} Links', + 'messagePreview.latestMessagePreview.messageFailedSend.text': + 'Nachricht konnte nicht gesendet werden', + 'messagePreview.latestMessagePreview.videoCount.label_one': 'Video', + 'messagePreview.latestMessagePreview.videoCount.label_other': '{{ count }} Videos', + 'messagePreview.latestMessagePreview.voiceMessageCount.label_one': 'Sprachnachricht', + 'messagePreview.latestMessagePreview.voiceMessageCount.label_other': + '{{ count }} Sprachnachrichten', + 'notification.attachmentFileMissing': 'Für den Anhang ist eine Datei erforderlich', + 'notification.attachmentIdMissing': 'Dem lokalen Anhang fehlt die lokale ID', + 'notification.attachmentUploadBlockedWithReason': + 'Anhang-Upload blockiert wegen {{reason}}', + 'notification.attachmentUploadFailed': 'Fehler beim Hochladen des Anhangs', + 'notification.attachmentUploadFailedWithReason': + 'Anhang-Upload fehlgeschlagen wegen {{reason}}', + 'notification.attachmentUploadInProgress': 'Warte, bis alle Anhänge hochgeladen sind', + 'notification.audioPlaybackError': 'Fehler beim Abspielen der Aufnahme', + 'notification.commandDisabled': 'Befehl nicht verfügbar', + 'notification.commandDisabledWhileEditing': 'Befehl beim Bearbeiten nicht verfügbar', + 'notification.commandDisabledWhileReplying': 'Befehl beim Antworten nicht verfügbar', + 'notification.dismissNotification.ariaLabel': 'Benachrichtigung schließen', + 'notification.jumpToFirstUnreadFailed': + 'Sprung zur ersten ungelesenen Nachricht fehlgeschlagen', + 'notification.list.notifications.ariaLabel': 'Benachrichtigungen', + 'notification.locationGetFailed': 'Standort konnte nicht ermittelt werden', + 'notification.locationShareFailed': 'Standort konnte nicht geteilt werden', + 'notification.pollCreateFailed': 'Umfrage konnte nicht erstellt werden', + 'notification.pollCreateFailedWithReason': + 'Umfrage konnte nicht erstellt werden wegen {{reason}}', + 'notification.pollEndFailed': 'Umfrage konnte nicht beendet werden', + 'notification.pollEndFailedWithReason': + 'Umfrage konnte nicht beendet werden wegen {{reason}}', + 'notification.pollEndSuccess': 'Umfrage beendet', + 'notification.pollVoteLimit': + 'Stimmenlimit erreicht. Entferne zuerst eine vorhandene Stimme.', + 'notification.reason.sizeLimit': 'Größenbeschränkung', + 'notification.reason.unknownError': 'unbekannter Fehler', + 'notification.reason.unsupportedFileType': 'nicht unterstützter Dateityp', + 'notification.replySearchFailed': 'Thread wurde nicht gefunden', + 'poll.actions.suggestOption.label': 'Option vorschlagen', + 'poll.actions.viewComments.label_one': '{{count}} Kommentar ansehen', + 'poll.actions.viewComments.label_other': '{{count}} Kommentare ansehen', + 'poll.actions.viewResults.label': 'Ergebnisse ansehen', + 'poll.addCommentPrompt.addComment.label': 'Kommentar hinzufügen', + 'poll.addCommentPrompt.addCommentPollAnswer.label': + 'Füge deiner Umfrageantwort einen Kommentar hinzu', + 'poll.addCommentPrompt.fieldCannotEmptyContain.label': + 'Dieses Feld darf nicht leer sein und nicht nur Leerzeichen enthalten', + 'poll.addCommentPrompt.update.text': 'Aktualisieren', + 'poll.addCommentPrompt.updateComment.label': 'Kommentar aktualisieren', + 'poll.addCommentPrompt.updateCommentAttachedPoll.label': + 'Aktualisiere den Kommentar zu deiner Umfrageantwort', + 'poll.answerList.description': 'Kommentare zu den Umfrageantworten ansehen', + 'poll.answerList.pollComments.title': 'Umfragekommentare', + 'poll.creationDialog.allowOthersAddComments.description': + 'Anderen erlauben, Kommentare hinzuzufügen', + 'poll.creationDialog.anonymousPoll.title': 'Anonyme Umfrage', + 'poll.creationDialog.createPoll.title': 'Umfrage erstellen', + 'poll.creationDialog.hideWhoVoted.description': 'Verbergen, wer abgestimmt hat', + 'poll.creationDialog.letOthersAddOptions.description': + 'Anderen erlauben, Optionen hinzuzufügen', + 'poll.creationDialog.pollSent.text': 'Umfrage gesendet', + 'poll.creationDialog.sendPoll.text': 'Umfrage senden', + 'poll.endPollAlert.description': + 'Möchtest du diese Umfrage jetzt beenden? Danach kann niemand mehr abstimmen.', + 'poll.endPollAlert.endPoll.text': 'Umfrage beenden', + 'poll.endPollAlert.endPoll.title': 'Diese Umfrage beenden?', + 'poll.header.selectOne.label': 'Wähle eine Option', + 'poll.header.selectOneMore.label': 'Wähle eine oder mehrere Optionen', + 'poll.header.selectUp.label_one': 'Wähle bis zu {{count}}', + 'poll.header.selectUp.label_other': 'Wähle bis zu {{count}}', + 'poll.header.voteEnded.label': 'Abstimmung beendet', + 'poll.multipleAnswersField.chooseBetween210.description': + 'Wähle zwischen 2 und 10 Optionen', + 'poll.multipleAnswersField.enforceUniqueVoteEnabled.label': + 'Eindeutige Stimme erzwingen ist aktiviert', + 'poll.multipleAnswersField.limitVotesPerPerson.title': 'Stimmen pro Person begrenzen', + 'poll.multipleAnswersField.maximumVotesPerPerson.ariaLabel': + 'Maximale Stimmen pro Person', + 'poll.multipleAnswersField.multipleVotes.title': 'Mehrere Stimmen', + 'poll.multipleAnswersField.onlyNumbersAllowed.label': 'Nur Zahlen sind erlaubt', + 'poll.multipleAnswersField.selectMoreThanOne.description': + 'Mehr als eine Option auswählen', + 'poll.multipleAnswersField.typeNumber210.label': 'Gib eine Zahl von 2 bis 10 ein', + 'poll.nameField.askQuestion.placeholder': 'Stelle eine Frage', + 'poll.nameField.error.text': 'Fehler', + 'poll.nameField.questionRequired.label': 'Eine Frage ist erforderlich', + 'poll.optionFieldSet.addOption.placeholder': 'Option hinzufügen', + 'poll.optionFieldSet.option.ariaLabel': 'Option {{ position }}', + 'poll.optionFieldSet.optionCanReorderedRemoved.ariaLabel': + 'Diese Option kann neu angeordnet und entfernt werden.', + 'poll.optionFieldSet.optionEmpty.label': 'Option ist leer', + 'poll.optionFieldSet.options.label': 'Optionen', + 'poll.optionFieldSet.optionsCanNowReordered.ariaLabel': + 'Optionen können jetzt neu angeordnet und entfernt werden.', + 'poll.optionFieldSet.removeOption.ariaLabel': 'Option entfernen: {{ option }}', + 'poll.optionList.moreOptions.label_one': '+{{count}} weitere Option', + 'poll.optionList.moreOptions.label_other': '+{{count}} weitere Optionen', + 'poll.optionReorder.pressSpaceSelectOption.ariaLabel': + 'Drücke die Leertaste, um diese Option auszuwählen, verschiebe sie mit den Pfeiltasten nach oben und unten und drücke die Leertaste erneut, um die Auswahl aufzuheben.', + 'poll.optionReorder.reorderOption.ariaLabel': 'Option {{ position }} neu anordnen', + 'poll.optionReorder.reorderPosition.ariaLabel': + '„{{ option }}“ an Position {{ position }} von {{ total }} neu anordnen', + 'poll.optionVotes.question.text': 'Frage {{ optionOrderNumber}}', + 'poll.optionVotes.view.text': 'Alle ansehen', + 'poll.optionVotes.votes.text_one': '{{count}} Stimme', + 'poll.optionVotes.votes.text_other': '{{count}} Stimmen', + 'poll.optionsFull.description': 'Alle in dieser Umfrage verfügbaren Optionen ansehen', + 'poll.optionsFull.pollOptions.title': 'Umfrageoptionen', + 'poll.pollComment.placeholder': 'Dein Kommentar', + 'poll.pollOptionSuggestion.placeholder': 'Neue Option eingeben', + 'poll.question.question.text': 'Frage', + 'poll.results.pollResults.title': 'Umfrageergebnisse', + 'poll.results.reviewPollResultsOpen.description': + 'Umfrageergebnisse ansehen und eine Option öffnen, um die Stimmen im Detail zu sehen', + 'poll.results.reviewWhoVotedOption.description': + 'Ansehen, wer für diese Option gestimmt hat', + 'poll.results.totalVoteCount.text_one': '1 Stimme insgesamt', + 'poll.results.totalVoteCount.text_other': '{{ count }} Stimmen insgesamt', + 'poll.results.votes.title': 'Stimmen', + 'poll.suggestPollOption.description': 'Eine neue Option für diese Umfrage vorschlagen', + 'poll.suggestPollOption.optionAlreadyExists.label': 'Option existiert bereits', + 'reactions.fetchReactions.errorFetchingReactions.text': + 'Fehler beim Laden der Reaktionen', + 'reactions.messageReactions.reactionList.ariaLabel': 'Reaktionsliste', + 'reactions.messageReactions.selectReaction.ariaLabel': + 'Reaktion auswählen: {{ reactionName }}', + 'reactions.messageReactionsDetail.reactions.text_one': '{{ count }} Reaktion', + 'reactions.messageReactionsDetail.reactions.text_other': '{{ count }} Reaktionen', + 'reactions.messageReactionsDetail.tapRemove.ariaLabel': + 'Zum Entfernen tippen: {{ reactionName }}', + 'reactions.messageReactionsDetail.tapRemove.text': 'Zum Entfernen tippen', + 'relativeTime.daysAgo_one': 'vor {{ count }} Tg.', + 'relativeTime.daysAgo_other': 'vor {{ count }} Tg.', + 'relativeTime.today': 'Heute', + 'relativeTime.weeksAgo_one': 'vor {{ count }} Wo.', + 'relativeTime.weeksAgo_other': 'vor {{ count }} Wo.', + 'relativeTime.yesterday': 'Gestern', + 'search.bar.clearSearch.ariaLabel': 'Suche leeren', + 'search.bar.exitSearch.ariaLabel': 'Suche beenden', + 'search.resultItem.selectUserChannel.ariaLabel': 'Benutzerkanal auswählen: {{ name }}', + 'search.results.searchResults.ariaLabel': 'Suchergebnisse', + 'search.resultsHeader.ariaLabel': + 'Filterschaltfläche der Suchergebnisse für: {{ source }}', + 'search.resultsHeader.filterSource.channels': 'Kanäle', + 'search.resultsHeader.filterSource.messages': 'Nachrichten', + 'search.resultsHeader.filterSource.users': 'Benutzer', + 'search.resultsPresearch.startTypingSearch.text': 'Tippen, um zu suchen', + 'search.sourceResults.noResultsFound.text': 'Keine Ergebnisse gefunden', + 'search.sourceResults.searching.text': 'Suche nach {{ searchSourceType }}...', + 'slotLayout.chatView.channels.text': 'Kanäle', + 'slotLayout.chatView.chatViewControls.ariaLabel': 'Chat-Ansichtssteuerung', + 'slotLayout.chatView.openChannelsView.ariaLabel': 'Kanalansicht öffnen', + 'slotLayout.chatView.openThreadsView.ariaLabel': 'Thread-Ansicht öffnen', + 'slotLayout.chatView.openThreadsViewUnread.ariaLabel_one': + 'Thread-Ansicht öffnen, {{ count }} ungelesener Thread', + 'slotLayout.chatView.openThreadsViewUnread.ariaLabel_other': + 'Thread-Ansicht öffnen, {{ count }} ungelesene Threads', + 'textareaComposer.messageInput.ariaLabel': 'Nachrichteneingabe', + 'textareaComposer.roleItem.notifyMembers.label': + 'Alle {{ role }}-Mitglieder benachrichtigen', + 'textareaComposer.suggestionList.commandSuggestions.ariaLabel': 'Befehlsvorschläge', + 'textareaComposer.suggestionList.emojiSuggestions.ariaLabel': 'Emoji-Vorschläge', + 'textareaComposer.suggestionList.mentionSuggestions.ariaLabel': 'Erwähnungsvorschläge', + 'textareaComposer.suggestionList.suggestions.ariaLabel': 'Vorschläge', + 'textareaComposer.textareaPlaceholder.searchGiFs.label': 'GIFs suchen', + 'textareaComposer.textareaPlaceholder.sendMessage.label': 'Nachricht schreiben', + 'textareaComposer.textareaPlaceholder.slowModeWaitS.label': + 'Langsamer Modus, warte {{ seconds }}s...', + 'thread.header.closeThread.ariaLabel': 'Thread schließen', + 'thread.header.thread.text': 'Thread', + 'threadList.chat.ariaLabel': 'Chat: {{ channelName }}', + 'threadList.empty.text': 'Antworte auf eine Nachricht, um einen Thread zu starten', + 'threadList.thread.ariaLabel': 'Thread: {{ messagePreview }}', + 'threadList.threadList.ariaLabel': 'Thread-Liste', + 'threadList.unseenBanner.loading': 'Wird geladen...', + 'threadList.unseenBanner.unreadThreads_one': '{{ count }} ungelesener Thread', + 'threadList.unseenBanner.unreadThreads_other': '{{ count }} ungelesene Threads', + 'typing.manyUsers_one': '{{ count }} Person schreibt', + 'typing.manyUsers_other': '{{ count }} Personen schreiben', + 'typing.singleUser': '{{ typing }} schreibt', + 'typing.twoUsers': '{{ typing }} schreiben', + 'videoPlayer.videoThumbnail.playVideo.ariaLabel': 'Video abspielen', + + // The four keys below need translating even though they look like format expressions rather than + // copy. dayjs takes the calendar wording as part of the format string (square brackets escape + // literal text), so the day words are baked in — and because a per-key `calendarFormats` argument + // replaces the locale's own calendar wholesale, the `calendar` block below cannot reach them. + // Skip these and a translated app keeps rendering "Today" in its date separators. Everything + // outside the brackets is a format token (LT, L, dddd, MMM) that dayjs localizes from the locale + // imported at the top of this file. + 'timestamp.ChannelDetailPinnedMessageTimestamp': + '{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { "sameDay": "LT", "lastDay": "[Gestern]", "lastWeek": "dddd", "sameElse": "L" }) }}', + 'timestamp.ChannelPreviewTimestamp': + '{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { "sameDay": "LT", "lastDay": "[Gestern]", "lastWeek": "dddd", "sameElse": "L" }) }}', + 'timestamp.DateSeparator': + '{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { "sameDay": "[Heute]", "nextDay": "[Morgen]", "lastDay": "[Gestern]", "nextWeek": "dddd", "lastWeek": "[Letzten] dddd", "sameElse": "ddd, D. MMM" }) }}', + 'timestamp.ReminderNotification': + '{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { "sameDay": "[Heute] [um] HH:mm", "nextDay": "[Morgen] [um] HH:mm", "lastDay": "[Gestern] [um] HH:mm", "nextWeek": "dddd [um] HH:mm", "lastWeek": "[Letzten] dddd [um] HH:mm", "sameElse": "ddd, D. MMM [um] HH:mm" }) }}', +} as const satisfies TranslationDictionary; + +/** + * Every key that is copy — the formatter expressions the SDK resolves from its own bundled + * defaults are not something an integrator translates. + */ +type TranslatableKey = Exclude< + keyof TranslationCatalog, + `duration.${string}` | `timestamp.${string}` | `translationBuilderTopic.${string}` +>; + +/** + * Compile-time completeness gate. `as const satisfies` above keeps the literal keys that + * `keyof typeof` needs while still checking each one against the catalog, so this resolves to + * `never` only when nothing is left untranslated. Add a key to the SDK without translating it here + * and the error names it. + */ +type Untranslated = Exclude; +type AssertNoneMissing = T; +export type GermanIsComplete = AssertNoneMissing; + +/** + * Calendar wording for the keys that format against the locale's own calendar — + * `timestamp.LiveLocation` and `timestamp.PollVoteTooltip`. Passed as the third argument to + * `registerTranslation`, which is how a shared instance carries one config per language. + */ +export const deDayjsLocaleConfig = { + calendar: { + lastDay: '[gestern um] LT', + lastWeek: '[letzten] dddd [um] LT', + nextDay: '[morgen um] LT', + nextWeek: 'dddd [um] LT', + sameDay: '[heute um] LT', + sameElse: 'L', + }, +}; diff --git a/examples/vite/src/i18n/index.ts b/examples/vite/src/i18n/index.ts new file mode 100644 index 0000000000..a17c48b0c5 --- /dev/null +++ b/examples/vite/src/i18n/index.ts @@ -0,0 +1,51 @@ +// Adding languages to the SDK, end to end. English is the only one it bundles; everything else is +// three things you supply per language: +// +// 1. a dictionary of translated keys — `Translations` +// 2. the dayjs locale, for month/weekday names — `import 'dayjs/locale/.js'` +// 3. a `calendar` config, for relative date wording — `DayjsLocaleConfig` +// +// Steps 1 and 3 are the exports of ./de.ts and ./it.ts; step 2 is the side-effect import at the top +// of each. All of it goes onto **one** `Streami18n` instance: `registerTranslation` takes the dayjs +// config as its third argument, so every language is registered up front and `setLanguage()` swaps +// the active one at runtime — no remount, no second instance. The language switcher in +// AppSettings › General drives exactly that call. +import { Streami18n } from 'stream-chat-react'; + +import { deDayjsLocaleConfig, deTranslations } from './de'; +import { itDayjsLocaleConfig, itTranslations } from './it'; + +const registeredLanguages = { + de: { dayjsLocaleConfig: deDayjsLocaleConfig, translations: deTranslations }, + it: { dayjsLocaleConfig: itDayjsLocaleConfig, translations: itTranslations }, +}; + +/** The languages the switcher offers. `en` needs no dictionary — the SDK ships it inline. */ +export const availableLanguages = [ + { code: 'en', label: 'English' }, + { code: 'de', label: 'Deutsch' }, + { code: 'it', label: 'Italiano' }, +] as const; + +export const DEFAULT_LANGUAGE = 'en'; + +const languageFromUrl = + typeof window === 'undefined' + ? null + : new URLSearchParams(window.location.search).get('language'); + +/** + * The app's single `Streami18n` instance, with every language registered. + * + * A code with no dictionary still works: the UI keeps the SDK's English copy while dates follow + * that language, provided its dayjs locale has been imported. + */ +export const streamI18n = new Streami18n({ + language: languageFromUrl ?? DEFAULT_LANGUAGE, +}); + +for (const [code, { dayjsLocaleConfig, translations }] of Object.entries( + registeredLanguages, +)) { + streamI18n.registerTranslation(code, translations, dayjsLocaleConfig); +} diff --git a/examples/vite/src/i18n/it.ts b/examples/vite/src/i18n/it.ts new file mode 100644 index 0000000000..f2878b7cec --- /dev/null +++ b/examples/vite/src/i18n/it.ts @@ -0,0 +1,802 @@ +// Italian for the example app. Same shape as ./de.ts — see that file for the annotated version. +import 'dayjs/locale/it.js'; + +import type { TranslationCatalog, TranslationDictionary } from 'stream-chat-react'; + +export const itTranslations = { + 'a11y.accessibleLabel.active.ariaLabel': 'Attivo', + 'a11y.accessibleLabel.unreadMessage.ariaLabel_one': '{{ count }} messaggio non letto', + 'a11y.accessibleLabel.unreadMessage.ariaLabel_other': '{{ count }} messaggi non letti', + 'a11y.incomingMessageAnnouncements.newMessage.label': 'Nuovo messaggio da {{user}}', + 'a11y.interactionAnnouncements.commandActivated.ariaLabel': + 'Comando attivato: {{ command }}', + 'a11y.interactionAnnouncements.droppedPosition.ariaLabel': + '"{{ option }}" rilasciato in posizione {{ position }}.', + 'a11y.interactionAnnouncements.giphyCanceled.ariaLabel': 'Giphy annullato', + 'a11y.interactionAnnouncements.giphyImageChanged.ariaLabel': 'Immagine Giphy cambiata', + 'a11y.interactionAnnouncements.giphyImageChanged.withTitle.ariaLabel': + 'Immagine Giphy cambiata: {{ title }}', + 'a11y.interactionAnnouncements.giphySent.ariaLabel': 'Giphy inviato', + 'a11y.interactionAnnouncements.noSearchResultsFound.ariaLabel': + 'Nessun risultato di ricerca', + 'a11y.interactionAnnouncements.openedChannel.ariaLabel': 'Canale aperto: {{ name }}', + 'a11y.interactionAnnouncements.openedThread.ariaLabel': 'Thread aperto in {{ name }}', + 'a11y.interactionAnnouncements.pickedUpUseArrow.ariaLabel': + '"{{ option }}" selezionato. Usa le frecce per riordinare. Premi Spazio o Tab per rilasciare.', + 'a11y.interactionAnnouncements.pollDialogOpened.ariaLabel': + 'Finestra del sondaggio aperta', + 'a11y.interactionAnnouncements.pollSent.ariaLabel': 'Sondaggio inviato', + 'a11y.interactionAnnouncements.pressEnterStartTyping.ariaLabel': + 'Premi Invio per iniziare a scrivere', + 'a11y.interactionAnnouncements.recordingPaused.ariaLabel': 'Registrazione in pausa', + 'a11y.interactionAnnouncements.recordingResumed.ariaLabel': 'Registrazione ripresa', + 'a11y.interactionAnnouncements.recordingStarted.ariaLabel': 'Registrazione avviata', + 'a11y.interactionAnnouncements.removedOption.ariaLabel': 'Opzione {{ option }} rimossa', + 'a11y.interactionAnnouncements.searchCleared.ariaLabel': 'Ricerca cancellata', + 'a11y.interactionAnnouncements.searchResults.ariaLabel_one': '{{ count }} risultato', + 'a11y.interactionAnnouncements.searchResults.ariaLabel_other': '{{ count }} risultati', + 'a11y.interactionAnnouncements.suggestions.ariaLabel_one': '{{ count }} suggerimento', + 'a11y.interactionAnnouncements.suggestions.ariaLabel_other': '{{ count }} suggerimenti', + 'a11y.interactionAnnouncements.suggestionsWithLabel.ariaLabel_one': + '{{ count }} {{ suggestionsLabel }}', + 'a11y.interactionAnnouncements.suggestionsWithLabel.ariaLabel_other': + '{{ count }} {{ suggestionsLabel }}', + 'a11y.interactionAnnouncements.userSelected.ariaLabel': + 'Utente selezionato: {{ user }}', + 'a11y.interactionAnnouncements.voiceMessageSent.ariaLabel': 'Messaggio vocale inviato', + 'a11y.interactionAnnouncements.voiceRecordingAttached.ariaLabel': + 'Registrazione vocale allegata', + 'aiState.indicator.generating.label': 'Generazione in corso...', + 'aiState.indicator.thinking.label': 'Sto pensando...', + 'attachment.actions.giphyActions.ariaLabel': 'Azioni Giphy', + 'attachment.actions.giphyPreviewOnlyVisible.ariaLabel': + 'Anteprima Giphy, visibile solo a te. Usa le azioni Invia, Mescola o Annulla.', + 'attachment.actions.shuffle.label': 'Mescola', + 'attachment.geolocation.liveUntil.text': 'In diretta fino a {{ timestamp }}', + 'attachment.geolocation.locationSharingEnded.text': + 'Condivisione della posizione terminata', + 'attachment.geolocation.openLocationMap.ariaLabel': 'Apri la posizione su una mappa', + 'attachment.geolocation.stopSharing.text': 'Interrompi la condivisione', + 'attachment.giphy.animatedGif.ariaLabel': 'GIF animata', + 'attachment.giphy.animatedGif.withTitle.ariaLabel': 'GIF animata: {{ title }}', + 'attachment.modalGallery.openGalleryImage.label': + 'Apri la galleria all’immagine {{ index }}', + 'attachment.modalGallery.openImageGallery.label': 'Apri l’immagine nella galleria', + 'attachment.unableRenderCard.text': 'questo contenuto non può essere visualizzato', + 'attachment.visibilityDisclaimer.onlyVisible.text': 'Visibile solo a te', + 'audioPlayback.audioPlayerNotifications.cannotSeekRecording.label': + 'Impossibile spostarsi nella registrazione', + 'audioPlayback.audioPlayerNotifications.failedPlayRecording.label': + 'Impossibile riprodurre la registrazione', + 'audioPlayback.audioPlayerNotifications.recordingFormatNotSupported.label': + 'Il formato della registrazione non è supportato e non può essere riprodotto', + 'audioPlayback.progressBar.seekAudioPosition.ariaLabel': 'Cerca posizione audio', + 'audioPlayback.progressBarA11y.audioPosition.ariaLabel': + 'Posizione audio {{ elapsed }} di {{ duration }}', + 'audioPlayback.progressBarA11y.audioPositionPercent.ariaLabel': + 'Posizione audio {{ progress }} percento', + 'baseImage.imagePlaceholder.imageFailedLoad.ariaLabel': + 'Impossibile caricare l’immagine', + 'channel.channelMissing.text': 'Canale mancante', + 'channelDetail.avatarChannelDetail.channelDetails.ariaLabel': 'Dettagli del canale', + 'channelDetail.avatarChannelDetail.openChannelDetails.ariaLabel': + 'Apri i dettagli del canale', + 'channelDetail.channelFilesEmpty.noFiles.text': 'Nessun file', + 'channelDetail.channelFilesEmpty.shareFileSee.text': + 'Condividi un file per vederlo qui', + 'channelDetail.channelFilesView.files.title': 'File', + 'channelDetail.channelManagementActions.blockUser.title': 'Blocca utente', + 'channelDetail.channelManagementActions.chatDeleted.text': 'Chat eliminata', + 'channelDetail.channelManagementActions.deleteChat.title': 'Elimina chat', + 'channelDetail.channelManagementActions.errorBlockingUser.text': + 'Errore durante il blocco dell’utente', + 'channelDetail.channelManagementActions.errorDeletingChat.text': + 'Errore durante l’eliminazione della chat', + 'channelDetail.channelManagementActions.errorMutingChannel.text': + 'Errore durante il silenziamento del canale', + 'channelDetail.channelManagementActions.errorMutingUser.text': + 'Errore durante il silenziamento dell’utente', + 'channelDetail.channelManagementActions.errorUnblockingUser.text': + 'Errore durante lo sblocco dell’utente', + 'channelDetail.channelManagementActions.errorUnmutingChannel.text': + 'Errore durante la riattivazione del canale', + 'channelDetail.channelManagementActions.errorUnmutingUser.text': + 'Errore durante la riattivazione dell’utente', + 'channelDetail.channelManagementActions.leaveChat.title': 'Lascia la chat', + 'channelDetail.channelManagementActions.muteChat.title': 'Silenzia chat', + 'channelDetail.channelManagementActions.muteUser.title': 'Silenzia utente', + 'channelDetail.channelManagementActions.permanentlyDeletesMessageHistory.description': + 'Questo elimina definitivamente la cronologia dei messaggi con {{ user }}. L’azione non può essere annullata.', + 'channelDetail.channelManagementActions.sureWantLeaveChannel.description': + 'Vuoi davvero lasciare questo canale?', + 'channelDetail.channelManagementActions.unmuteChat.title': 'Riattiva chat', + 'channelDetail.channelManagementActions.unmuteUser.title': 'Riattiva utente', + 'channelDetail.channelManagementActions.userAbleMessageAgain.description': + 'Questo utente potrà scriverti di nuovo.', + 'channelDetail.channelManagementActions.userMuted.text': 'Utente silenziato', + 'channelDetail.channelManagementActions.userUnmuted.text': 'Utente riattivato', + 'channelDetail.channelManagementActions.userWonTAble.description': + 'Questo utente non potrà più scriverti. Puoi sbloccarlo in qualsiasi momento.', + 'channelDetail.channelManagementView.changesSaved.text': 'Modifiche salvate', + 'channelDetail.channelManagementView.contactInfo.label': 'Informazioni contatto', + 'channelDetail.channelManagementView.contactName.label': 'Nome del contatto', + 'channelDetail.channelManagementView.edit.text': 'Modifica', + 'channelDetail.channelManagementView.editChatData.ariaLabel': + 'Modifica i dati della chat', + 'channelDetail.channelManagementView.editContact.label': 'Modifica contatto', + 'channelDetail.channelManagementView.editGroup.label': 'Modifica gruppo', + 'channelDetail.channelManagementView.failedSaveChanges.text': + 'Impossibile salvare le modifiche', + 'channelDetail.channelManagementView.groupInfo.label': 'Informazioni gruppo', + 'channelDetail.channelManagementView.groupName.label': 'Nome del gruppo', + 'channelDetail.channelManagementView.manageChannel.description': 'Gestisci canale', + 'channelDetail.channelManagementView.save.text': 'Salva', + 'channelDetail.channelManagementView.uploadPicture.text': 'Carica immagine', + 'channelDetail.channelMediaEmpty.noPhotosVideos.text': 'Nessuna foto o video', + 'channelDetail.channelMediaEmpty.sharePhotoVideoSee.text': + 'Condividi una foto o un video per vederlo qui', + 'channelDetail.channelMediaView.next.text': 'Avanti', + 'channelDetail.channelMediaView.nextPage.ariaLabel': 'Pagina successiva', + 'channelDetail.channelMediaView.openImageShared.ariaLabel': + 'Apri l’immagine condivisa da {{ name }}', + 'channelDetail.channelMediaView.openVideoShared.ariaLabel': + 'Apri il video condiviso da {{ name }}', + 'channelDetail.channelMediaView.photosVideos.title': 'Foto e video', + 'channelDetail.channelMediaView.previous.text': 'Indietro', + 'channelDetail.channelMediaView.previousPage.ariaLabel': 'Pagina precedente', + 'channelDetail.channelMemberActions.ableMessageAgain.description': + '{{ member }} potrà scriverti di nuovo.', + 'channelDetail.channelMemberActions.errorOpeningDirectMessage.text': + 'Errore durante l’apertura del messaggio diretto', + 'channelDetail.channelMemberActions.errorRemovingUser.text': + 'Errore durante la rimozione dell’utente', + 'channelDetail.channelMemberActions.removeChannel.description': + 'Rimuovere {{ member }} da questo canale?', + 'channelDetail.channelMemberActions.removeUser.title': 'Rimuovi utente', + 'channelDetail.channelMemberActions.sendDirectMessage.title': 'Invia messaggio diretto', + 'channelDetail.channelMemberActions.unblockUser.title': 'Sblocca utente', + 'channelDetail.channelMemberActions.userRemoved.text': 'Utente rimosso', + 'channelDetail.channelMemberActions.wonTAbleMessage.description': + '{{ member }} non potrà più scriverti.', + 'channelDetail.channelMemberDetail.lastSeen.label': 'Ultimo accesso {{ timestamp }}', + 'channelDetail.channelMemberDetail.memberDetail.title': 'Dettagli del membro', + 'channelDetail.channelMembersAdd.addMembers.text_one': 'Aggiungi {{ count }} membro', + 'channelDetail.channelMembersAdd.addMembers.text_other': 'Aggiungi {{ count }} membri', + 'channelDetail.channelMembersAdd.alreadyMember.label': 'Già membro', + 'channelDetail.channelMembersAdd.errorAddingMembers.text': + 'Errore durante l’aggiunta dei membri', + 'channelDetail.channelMembersAdd.membersAdded.text_one': '{{ count }} membro aggiunto', + 'channelDetail.channelMembersAdd.membersAdded.text_other': + '{{ count }} membri aggiunti', + 'channelDetail.channelMembersAdd.noUserFound.text': 'Nessun utente trovato', + 'channelDetail.channelMembersBrowse.admin.label': 'Amministratore', + 'channelDetail.channelMembersBrowse.moderator.label': 'Moderatore', + 'channelDetail.channelMembersBrowse.noMemberFound.text': 'Nessun membro trovato', + 'channelDetail.channelMembersBrowse.owner.label': 'Proprietario', + 'channelDetail.channelMembersBrowse.viewMemberDetails.ariaLabel': + 'Vedi i dettagli del membro {{ member }}', + 'channelDetail.channelMembersHeader.actions.text': 'Azioni', + 'channelDetail.channelMembersHeader.add.text': 'Aggiungi', + 'channelDetail.channelMembersHeader.addChannelMembers.ariaLabel': + 'Aggiungi membri al canale', + 'channelDetail.channelMembersHeader.openMembersActions.ariaLabel': + 'Apri le azioni sui membri', + 'channelDetail.channelMembersView.addMembers.label': 'Aggiungi membri', + 'channelDetail.channelMembersView.browseChannelMembers.description': + 'Sfoglia i membri del canale', + 'channelDetail.channelMembersView.members.title_one': '{{ count }} membro', + 'channelDetail.channelMembersView.members.title_other': '{{ count }} membri', + 'channelDetail.pinnedMessagesEmpty.noPinnedMessages.text': 'Nessun messaggio fissato', + 'channelDetail.pinnedMessagesEmpty.pinMessageSee.text': + 'Fissa un messaggio per vederlo qui', + 'channelDetail.pinnedMessagesView.browsePinnedMessages.description': + 'Sfoglia i messaggi fissati', + 'channelDetail.pinnedMessagesView.noMessagesFound.text': 'Nessun messaggio trovato', + 'channelDetail.pinnedMessagesView.pinnedMessage.label': 'Messaggio fissato', + 'channelDetail.pinnedMessagesView.pinnedMessages.title': 'Messaggi fissati', + 'channelDetail.sectionNavigatorHeader.openMenu.ariaLabel': 'Apri menu', + 'channelHeader.online.members.label': '{{ memberCount }} membri', + 'channelHeader.online.online.label': '{{ watcherCount }} online', + 'channelList.channelList.ariaLabel': 'Elenco canali', + 'channelList.header.chats.text': 'Chat', + 'channelListItem.archive.title': 'Archivia', + 'channelListItem.attachment.ariaLabel': 'Allegato', + 'channelListItem.attachment.text': '🏙 Allegato...', + 'channelListItem.attachment.withAttachmentType.ariaLabel': + 'Allegato {{ attachmentType }}', + 'channelListItem.attachmentCount.ariaLabel_one': '{{ count }} allegato', + 'channelListItem.attachmentCount.ariaLabel_other': '{{ count }} allegati', + 'channelListItem.audio.ariaLabel': 'audio', + 'channelListItem.channelActions.ariaLabel': 'Azioni del canale', + 'channelListItem.channelArchived.text': 'Canale archiviato', + 'channelListItem.channelDisplayName.directMessage.label': 'Messaggio diretto', + 'channelListItem.channelPinned.text': 'Canale fissato', + 'channelListItem.channelUnarchived.text': 'Canale ripristinato dall’archivio', + 'channelListItem.channelUnpinned.text': 'Canale rimosso dai fissati', + 'channelListItem.created.text': '📊 {{createdBy}} ha creato: {{ pollName}}', + 'channelListItem.delivered.ariaLabel': 'Consegnato', + 'channelListItem.deliveryStatus.ariaLabel': 'Stato di consegna: {{ deliveryStatus }}', + 'channelListItem.failedBlockUser.text': 'Impossibile bloccare l’utente', + 'channelListItem.failedUpdateChannelArchive.text': + 'Impossibile aggiornare lo stato di archiviazione del canale', + 'channelListItem.failedUpdateChannelMute.text': + 'Impossibile aggiornare lo stato di silenziamento del canale', + 'channelListItem.failedUpdateChannelPinned.text': + 'Impossibile aggiornare lo stato dei canali fissati', + 'channelListItem.file.ariaLabel': 'file', + 'channelListItem.gif.ariaLabel': 'GIF', + 'channelListItem.image.ariaLabel': 'immagine', + 'channelListItem.lastMessage.withMessagePreview.ariaLabel': + 'Ultimo messaggio: {{ messagePreview }}', + 'channelListItem.lastMessage.withSenderAndMessagePreview.ariaLabel': + 'Ultimo messaggio da {{ sender }}: {{ messagePreview }}', + 'channelListItem.leaveChannel.title': 'Lascia il canale', + 'channelListItem.messageAttachments.ariaLabel': 'Messaggio con allegati', + 'channelListItem.noMessagesChat.ariaLabel': 'Non ci sono messaggi in questa chat.', + 'channelListItem.openChannelActionsMenu.ariaLabel': + 'Apri il menu delle azioni del canale', + 'channelListItem.poll.ariaLabel': 'Sondaggio: {{ pollName }}', + 'channelListItem.read.ariaLabel': 'Letto', + 'channelListItem.sent.ariaLabel': 'Inviato', + 'channelListItem.sharedLink.ariaLabel': 'Ha condiviso un link', + 'channelListItem.sharedLinkTitle.ariaLabel': + 'Ha condiviso un link con titolo: {{ linkTitle }}', + 'channelListItem.sharedLocation.ariaLabel': 'Posizione condivisa', + 'channelListItem.sharedLocation.text': '📍Posizione condivisa', + 'channelListItem.unarchive.title': 'Rimuovi dall’archivio', + 'channelListItem.unblockUser.title': 'Sblocca utente', + 'channelListItem.video.ariaLabel': 'video', + 'channelListItem.voiceMessage.ariaLabel': 'messaggio vocale', + 'channelListItem.voted.text': '📊 {{votedBy}} ha votato: {{pollOptionText}}', + 'chat.reportLostConnection.waitingNetwork.text': 'In attesa della rete…', + 'command.ban.args': '[@nomeutente] [testo]', + 'command.ban.description': 'Banna un utente', + 'command.giphy.args': '[testo]', + 'command.giphy.description': 'Pubblica una GIF casuale nel canale', + 'command.mute.args': '[@nomeutente]', + 'command.mute.description': 'Silenzia un utente', + 'command.unban.args': '[@nomeutente]', + 'command.unban.description': 'Rimuovi il ban di un utente', + 'command.unmute.args': '[@nomeutente]', + 'command.unmute.description': 'Riattiva un utente', + 'common.addReaction.text': 'Aggiungi reazione', + 'common.anonymous.label': 'Anonimo', + 'common.back.label': 'Indietro', + 'common.blockUser.title': 'Blocca utente', + 'common.cancel.label': 'Annulla', + 'common.channelMuted.text': 'Canale silenziato', + 'common.channelUnmuted.text': 'Canale riattivato', + 'common.close.ariaLabel': 'Chiudi', + 'common.createQuestionAddOptions.label': + 'Crea una domanda, aggiungi opzioni e configura le impostazioni del sondaggio', + 'common.currentLocation.text': 'Posizione attuale', + 'common.delete.text': 'Elimina', + 'common.downloadAttachment.ariaLabel': 'Scarica allegato', + 'common.downloadAttachment.title': 'Scarica allegato', + 'common.editMessage.text': 'Modifica messaggio', + 'common.emptyMessage.text': 'Messaggio vuoto...', + 'common.errorDeletingMessage.label': 'Errore durante l’eliminazione del messaggio', + 'common.errorMutingUser.label': 'Errore durante il silenziamento di un utente ...', + 'common.errorPinningMessage.label': 'Errore durante il fissaggio del messaggio', + 'common.errorRemovingMessagePin.label': + 'Errore durante la rimozione del messaggio fissato', + 'common.errorUnmutingUser.label': 'Errore durante la riattivazione di un utente ...', + 'common.failedLeaveChannel.text': 'Impossibile lasciare il canale', + 'common.lastActivity.ariaLabel': 'Ultima attività: {{ time }}', + 'common.leftChannel.text': 'Hai lasciato il canale', + 'common.liveLocation.text': 'Posizione in tempo reale', + 'common.location.text': 'Posizione', + 'common.messageDeleted.text': 'Messaggio eliminato', + 'common.messagePinned.label': 'Messaggio fissato', + 'common.mute.title': 'Silenzia', + 'common.muted.label': '{{ user }} è stato silenziato', + 'common.newMessages.label_one': '{{count}} nuovo messaggio', + 'common.newMessages.label_other': '{{count}} nuovi messaggi', + 'common.nothingYet.text': 'Ancora nulla...', + 'common.offline.label': 'Offline', + 'common.online.label': 'Online', + 'common.openReactionSelector.ariaLabel': 'Apri il selettore di reazioni', + 'common.pause.ariaLabel': 'Pausa', + 'common.pin.title': 'Fissa', + 'common.play.ariaLabel': 'Riproduci', + 'common.playbackSpeedX.label': 'Velocità di riproduzione {{ rate }}x', + 'common.poll.label': 'Sondaggio', + 'common.reminderSet.text': 'Promemoria impostato', + 'common.replyCount.label_one': '1 risposta', + 'common.replyCount.label_other': '{{ count }} risposte', + 'common.resultsLoaded.label': 'Tutti i risultati caricati', + 'common.retryUpload.ariaLabel': 'Riprova il caricamento', + 'common.savedLater.text': 'Salvato per dopo', + 'common.search.ariaLabel': 'Cerca', + 'common.send.label': 'Invia', + 'common.threads.text': 'Thread', + 'common.unblock.ariaLabel': 'Sblocca', + 'common.unmute.title': 'Riattiva', + 'common.unmuted.label': '{{ user }} è stato riattivato', + 'common.unpin.title': 'Rimuovi dai fissati', + 'common.unsupportedAttachment.text': 'Allegato non supportato', + 'common.userBlocked.text': 'Utente bloccato', + 'common.userUnblocked.text': 'Utente sbloccato', + 'common.userUploadedContent.label': 'Contenuto caricato dall’utente', + 'common.voiceMessage.label': 'Messaggio vocale', + 'common.you.label': 'Tu', + 'dialog.callout.closeCalloutDialog.ariaLabel': 'Chiudi la finestra informativa', + 'dialog.contextMenu.backParentMenuButton.ariaLabel': 'Torna al menu principale', + 'dialog.contextMenu.submenu.ariaLabel': 'Sottomenu', + 'dialog.prompt.goBack.ariaLabel': 'Torna indietro', + 'dialog.viewer.closeDialog.ariaLabel': 'Chiudi la finestra', + 'emojiPicker.emojiPicker.ariaLabel': 'Selettore di emoji', + 'emptyState.indicator.noConversationsYet.label': 'Nessuna conversazione', + 'emptyState.indicator.noItemsExist.text': 'Nessun elemento presente', + 'emptyState.indicator.startConversation.label': + 'Invia un messaggio per iniziare la conversazione', + 'fileUpload.uploadButton.fileUpload.ariaLabel': 'Caricamento file', + 'form.numericInput.decreaseValue.ariaLabel': 'Diminuisci il valore', + 'form.numericInput.increaseValue.ariaLabel': 'Aumenta il valore', + 'form.switchField.disabled.ariaLabel': '{{ setting }} disattivato', + 'form.switchField.enabled.ariaLabel': '{{ setting }} attivato', + 'gallery.ui.nextImage.ariaLabel': 'Immagine successiva', + 'gallery.ui.previousImage.ariaLabel': 'Immagine precedente', + 'language.af': 'Afrikaans', + 'language.am': 'Amarico', + 'language.ar': 'Arabo', + 'language.az': 'Azerbaigiano', + 'language.bg': 'Bulgaro', + 'language.bn': 'Bengalese', + 'language.bs': 'Bosniaco', + 'language.cs': 'Ceco', + 'language.da': 'Danese', + 'language.de': 'Tedesco', + 'language.el': 'Greco', + 'language.en': 'Inglese', + 'language.es': 'Spagnolo', + 'language.es-MX': 'Spagnolo (Messico)', + 'language.et': 'Estone', + 'language.fa': 'Persiano', + 'language.fa-AF': 'Dari', + 'language.fi': 'Finlandese', + 'language.fr': 'Francese', + 'language.fr-CA': 'Francese (Canada)', + 'language.ha': 'Hausa', + 'language.he': 'Ebraico', + 'language.hi': 'Hindi', + 'language.hr': 'Croato', + 'language.ht': 'Creolo haitiano', + 'language.hu': 'Ungherese', + 'language.id': 'Indonesiano', + 'language.it': 'Italiano', + 'language.ja': 'Giapponese', + 'language.ka': 'Georgiano', + 'language.ko': 'Coreano', + 'language.lt': 'Lituano', + 'language.lv': 'Lettone', + 'language.ms': 'Malese', + 'language.nl': 'Olandese', + 'language.no': 'Norvegese', + 'language.pl': 'Polacco', + 'language.ps': 'Pashto', + 'language.pt': 'Portoghese', + 'language.ro': 'Romeno', + 'language.ru': 'Russo', + 'language.sk': 'Slovacco', + 'language.sl': 'Sloveno', + 'language.so': 'Somalo', + 'language.sq': 'Albanese', + 'language.sr': 'Serbo', + 'language.sv': 'Svedese', + 'language.sw': 'Swahili', + 'language.ta': 'Tamil', + 'language.th': 'Thai', + 'language.tl': 'Tagalog', + 'language.tr': 'Turco', + 'language.uk': 'Ucraino', + 'language.ur': 'Urdu', + 'language.vi': 'Vietnamita', + 'language.zh': 'Cinese (semplificato)', + 'language.zh-TW': 'Cinese (tradizionale)', + 'loadMore.button.loadMore.label': 'Carica altri', + 'loading.errorIndicator.error.text': 'Errore: {{ errorMessage }}', + 'loading.progressIndicators.percentComplete.ariaLabel': + '{{percent}} percento completato', + 'location.shareLocationDialog.attach.text': 'Allega', + 'location.shareLocationDialog.description': + 'Seleziona la tua posizione attuale e attiva facoltativamente la condivisione in tempo reale', + 'location.shareLocationDialog.share.text': 'Condividi', + 'location.shareLocationDialog.shareLiveLocation.title': + 'Condividi la posizione in tempo reale per', + 'location.shareLocationDialog.shareLocation.title': 'Condividi posizione', + 'mediaRecorder.audioRecorderRecording.cancelRecording.ariaLabel': + 'Annulla registrazione', + 'mediaRecorder.audioRecorderRecording.completeRecording.ariaLabel': + 'Completa registrazione', + 'mediaRecorder.audioRecorderRecording.pauseRecording.ariaLabel': + 'Metti in pausa la registrazione', + 'mediaRecorder.audioRecorderRecording.resumeRecording.ariaLabel': + 'Riprendi la registrazione', + 'mediaRecorder.audioRecorderRecording.voiceMessageDeleted.text': + 'Messaggio vocale eliminato', + 'mediaRecorder.audioRecordingButton.startRecordingAudio.ariaLabel': + 'Avvia la registrazione audio', + 'mediaRecorder.error.processing': + 'Si è verificato un errore durante l’elaborazione della registrazione', + 'mediaRecorder.error.recording': 'Si è verificato un errore durante la registrazione', + 'mediaRecorder.error.start': 'Errore durante l’avvio della registrazione', + 'mediaRecorder.permissionDenied.camera.body': + 'Per iniziare a registrare, consenti l’accesso alla fotocamera nel browser', + 'mediaRecorder.permissionDenied.camera.heading': 'Consenti l’accesso alla fotocamera', + 'mediaRecorder.permissionDenied.microphone.body': + 'Per iniziare a registrare, consenti l’accesso al microfono nel browser', + 'mediaRecorder.permissionDenied.microphone.heading': 'Consenti l’accesso al microfono', + 'mention.channel.description': 'Notifica tutti in questo canale', + 'mention.here.description': 'Notifica tutti i membri online in questo canale', + 'message.alsoSent.alsoSentChannel.text': 'Inviato anche nel canale', + 'message.alsoSent.repliedThread.text': 'Ha risposto a un thread', + 'message.alsoSent.view.text': 'Vedi', + 'message.and.withCommaSeparatedUsersAndLastUser.label': + '{{ commaSeparatedUsers }} e {{ lastUser }}', + 'message.and.withFirstUserAndSecondUser.label': '{{ firstUser }} e {{ secondUser }}', + 'message.blocked.text': 'Il messaggio è stato bloccato dalle norme di moderazione', + 'message.editedIndicator.edited.text': 'Modificato', + 'message.more.label': '{{ commaSeparatedUsers }} e altri {{ moreCount }}', + 'message.pinIndicator.pinned.label': 'Fissato da te', + 'message.pinIndicator.pinned.withName.label': 'Fissato da {{ name }}', + 'message.reminderNotification.due.label': 'In scadenza {{ timeLeft }}', + 'message.reminderNotification.dueSince.label': 'Scaduto da {{ dueSince }}', + 'message.status.delivered.text': 'Consegnato', + 'message.status.sending.text': 'Invio in corso...', + 'message.status.sent.text': 'Inviato', + 'message.text.message.ariaLabel': 'Messaggio,', + 'message.text.message.withUser.ariaLabel': 'Messaggio da {{ user }},', + 'message.translationIndicator.original.text': 'Originale', + 'message.translationIndicator.translated.text': 'Tradotto', + 'message.translationIndicator.translated.withLanguage.text': + 'Tradotto da {{ language }}', + 'message.translationIndicator.viewOriginal.text': 'Vedi originale', + 'message.translationIndicator.viewTranslation.text': 'Vedi traduzione', + 'message.ui.reviewBouncedMessage.ariaLabel': 'Rivedi il messaggio rifiutato', + 'messageActions.blockUser.ariaLabel': 'Blocca utente', + 'messageActions.bookmarkMessage.ariaLabel': 'Aggiungi il messaggio ai segnalibri', + 'messageActions.copyMessage.text': 'Copia messaggio', + 'messageActions.copyMessageText.ariaLabel': 'Copia il testo del messaggio', + 'messageActions.deleteMessage.ariaLabel': 'Elimina messaggio', + 'messageActions.deleteMessageAlert.deleteMessage.title': 'Elimina messaggio', + 'messageActions.deleteMessageAlert.description': + 'Vuoi davvero eliminare questo messaggio?', + 'messageActions.downloadSubmenu.download.label': 'Scarica {{ fileName }}', + 'messageActions.downloadSubmenu.download.text': 'Scarica tutto', + 'messageActions.downloadSubmenu.downloadAttachment.label': + 'Scarica allegato {{ number }}', + 'messageActions.editMessage.ariaLabel': 'Modifica messaggio', + 'messageActions.errorAddingFlag.text': 'Errore durante la segnalazione', + 'messageActions.errorMarkingMessageUnread.text': + 'Errore durante la marcatura come non letto. Non è possibile marcare come non letti i messaggi più vecchi dei 100 più recenti del canale.', + 'messageActions.flag.text': 'Segnala', + 'messageActions.flagMessage.ariaLabel': 'Segnala messaggio', + 'messageActions.markMessageUnread.ariaLabel': 'Segna il messaggio come non letto', + 'messageActions.markUnread.text': 'Segna come non letto', + 'messageActions.messageActions.ariaLabel': 'Azioni del messaggio', + 'messageActions.messageMarkedUnread.text': 'Messaggio segnato come non letto', + 'messageActions.messageSuccessfullyFlagged.text': + 'Il messaggio è stato segnalato con successo', + 'messageActions.messageUnpinned.text': 'Messaggio rimosso dai fissati', + 'messageActions.muteUser.ariaLabel': 'Silenzia utente', + 'messageActions.openMessageActionsMenu.ariaLabel': + 'Apri il menu delle azioni del messaggio', + 'messageActions.openThread.ariaLabel': 'Apri thread', + 'messageActions.pinMessage.ariaLabel': 'Fissa messaggio', + 'messageActions.quoteMessage.ariaLabel': 'Cita messaggio', + 'messageActions.quoteReply.text': 'Rispondi citando', + 'messageActions.remindMe.text': 'Ricordami', + 'messageActions.remindMeMessage.ariaLabel': 'Ricordami questo messaggio', + 'messageActions.remindMeSubmenu.remindMe.text': 'Ricordami', + 'messageActions.removeReminder.ariaLabel': 'Rimuovi promemoria', + 'messageActions.removeReminder.text': 'Rimuovi promemoria', + 'messageActions.removeSaveLater.ariaLabel': 'Rimuovi da Salvati per dopo', + 'messageActions.removeSaveLater.text': 'Rimuovi da salvati per dopo', + 'messageActions.resend.text': 'Invia di nuovo', + 'messageActions.resendMessage.ariaLabel': 'Invia di nuovo il messaggio', + 'messageActions.saveLater.text': 'Salva per dopo', + 'messageActions.threadReply.text': 'Rispondi nel thread', + 'messageActions.unmuteUser.ariaLabel': 'Riattiva utente', + 'messageActions.unpinMessage.ariaLabel': 'Rimuovi il messaggio dai fissati', + 'messageBounce.prompt.description': + 'Rivedi questo messaggio e scegli se eliminarlo, modificarlo o inviarlo comunque', + 'messageBounce.prompt.sendAnyway.text': 'Invia comunque', + 'messageBounce.prompt.title': + 'Questo messaggio non rispetta le nostre linee guida sui contenuti', + 'messageComposer.attachmentPreviewRoot.showPreview.ariaLabel': 'Mostra anteprima', + 'messageComposer.attachmentSelector.attachmentActions.ariaLabel': + 'Azioni sugli allegati', + 'messageComposer.attachmentSelector.commands.text': 'Comandi', + 'messageComposer.attachmentSelector.file.text': 'File', + 'messageComposer.attachmentSelector.openAttachmentSelector.ariaLabel': + 'Apri il selettore di allegati', + 'messageComposer.audioAttachmentPreview.fileTooLarge.text': 'File troppo grande', + 'messageComposer.audioAttachmentPreview.retryUpload.text': 'Riprova il caricamento', + 'messageComposer.audioAttachmentPreview.uploadBlocked.text': 'Caricamento bloccato', + 'messageComposer.audioAttachmentPreview.uploadError.text': 'Errore di caricamento', + 'messageComposer.audioAttachmentPreview.uploadFailed.text': 'Caricamento non riuscito', + 'messageComposer.commandChip.exitCommand.ariaLabel': 'Esci dal comando {{ command }}', + 'messageComposer.commandsMenu.backAttachments.ariaLabel': 'Torna agli allegati', + 'messageComposer.commandsMenu.instantCommands.text': 'Comandi rapidi', + 'messageComposer.dragDropUpload.dragFiles.text': 'Trascina qui i tuoi file', + 'messageComposer.dragDropUpload.someFilesNotAccepted.text': + 'Alcuni file non verranno accettati', + 'messageComposer.geolocationPreview.live.text': 'In diretta per {{duration}}', + 'messageComposer.geolocationPreview.location.text': 'Posizione: {{ coordinates }}', + 'messageComposer.geolocationPreview.removeLocationAttachment.ariaLabel': + 'Rimuovi l’allegato di posizione', + 'messageComposer.geolocationPreview.sharedLocation.title': 'Posizione condivisa', + 'messageComposer.icons.attachFiles.text': 'Allega file', + 'messageComposer.quotedMessagePreview.cancelReply.ariaLabel': 'Annulla risposta', + 'messageComposer.quotedMessagePreview.files.label_one': '{{ count }} file', + 'messageComposer.quotedMessagePreview.files.label_other': '{{ count }} file', + 'messageComposer.quotedMessagePreview.jumpQuotedMessage.ariaLabel': + 'Vai al messaggio citato', + 'messageComposer.quotedMessagePreview.photo.label': 'Foto', + 'messageComposer.quotedMessagePreview.photos.label_one': '{{ count }} foto', + 'messageComposer.quotedMessagePreview.photos.label_other': '{{ count }} foto', + 'messageComposer.quotedMessagePreview.reply.text': 'Rispondi', + 'messageComposer.quotedMessagePreview.reply.withAuthorName.text': + 'Rispondi a {{ authorName }}', + 'messageComposer.quotedMessagePreview.video.label': 'Video', + 'messageComposer.quotedMessagePreview.videos.label_one': '{{ count }} video', + 'messageComposer.quotedMessagePreview.videos.label_other': '{{ count }} video', + 'messageComposer.quotedMessagePreview.voiceMessage.label': + 'Messaggio vocale {{ duration }}', + 'messageComposer.removeAttachmentPreview.removeAttachment.ariaLabel': + 'Rimuovi allegato', + 'messageComposer.sendButton.send.ariaLabel': 'Invia', + 'messageComposer.sendChannelCheckbox.alsoSendChannel.label': 'Invia anche nel canale', + 'messageComposer.sendChannelCheckbox.alsoSendDirectMessage.label': + 'Invia anche come messaggio diretto', + 'messageComposer.sendMessageFn.sendMessageRequestFailed.text': + 'Invio del messaggio non riuscito', + 'messageComposer.stopAiGeneration.stopAiGeneration.ariaLabel': + 'Interrompi la generazione AI', + 'messageComposer.updateMessageFn.editMessageRequestFailed.text': + 'Modifica del messaggio non riuscita', + 'messageList.newMessageNotification.newMessages.label': 'Nuovi messaggi!', + 'messageList.scrollLatestMessage.jumpLatestMessage.ariaLabel': + 'Vai al messaggio più recente', + 'messageList.unreadMessagesNotification.markMessagesRead.ariaLabel': + 'Segna i messaggi come letti', + 'messageList.unreadMessagesNotification.unread.text_one': '{{count}} non letto', + 'messageList.unreadMessagesNotification.unread.text_other': '{{count}} non letti', + 'messageList.unreadMessagesNotification.unreadMessages.text': 'Messaggi non letti', + 'messagePreview.latestMessagePreview.fileCount.label_one': 'File', + 'messagePreview.latestMessagePreview.fileCount.label_other': '{{ count }} file', + 'messagePreview.latestMessagePreview.imageCount.label_one': 'Immagine', + 'messagePreview.latestMessagePreview.imageCount.label_other': '{{ count }} immagini', + 'messagePreview.latestMessagePreview.linkCount.label_one': 'Link', + 'messagePreview.latestMessagePreview.linkCount.label_other': '{{ count }} link', + 'messagePreview.latestMessagePreview.messageFailedSend.text': + 'Invio del messaggio non riuscito', + 'messagePreview.latestMessagePreview.videoCount.label_one': 'Video', + 'messagePreview.latestMessagePreview.videoCount.label_other': '{{ count }} video', + 'messagePreview.latestMessagePreview.voiceMessageCount.label_one': 'Messaggio vocale', + 'messagePreview.latestMessagePreview.voiceMessageCount.label_other': + '{{ count }} messaggi vocali', + 'notification.attachmentFileMissing': 'È necessario un file per l’allegato', + 'notification.attachmentIdMissing': 'All’allegato locale manca l’id locale', + 'notification.attachmentUploadBlockedWithReason': + 'Caricamento dell’allegato bloccato a causa di {{reason}}', + 'notification.attachmentUploadFailed': 'Errore durante il caricamento dell’allegato', + 'notification.attachmentUploadFailedWithReason': + 'Caricamento dell’allegato non riuscito a causa di {{reason}}', + 'notification.attachmentUploadInProgress': + 'Attendi il caricamento di tutti gli allegati', + 'notification.audioPlaybackError': 'Errore durante la riproduzione della registrazione', + 'notification.commandDisabled': 'Comando non disponibile', + 'notification.commandDisabledWhileEditing': + 'Comando non disponibile durante la modifica', + 'notification.commandDisabledWhileReplying': + 'Comando non disponibile durante la risposta', + 'notification.dismissNotification.ariaLabel': 'Chiudi la notifica', + 'notification.jumpToFirstUnreadFailed': + 'Impossibile passare al primo messaggio non letto', + 'notification.list.notifications.ariaLabel': 'Notifiche', + 'notification.locationGetFailed': 'Impossibile recuperare la posizione', + 'notification.locationShareFailed': 'Impossibile condividere la posizione', + 'notification.pollCreateFailed': 'Impossibile creare il sondaggio', + 'notification.pollCreateFailedWithReason': + 'Impossibile creare il sondaggio a causa di {{reason}}', + 'notification.pollEndFailed': 'Impossibile terminare il sondaggio', + 'notification.pollEndFailedWithReason': + 'Impossibile terminare il sondaggio a causa di {{reason}}', + 'notification.pollEndSuccess': 'Sondaggio terminato', + 'notification.pollVoteLimit': + 'Hai raggiunto il limite di voti. Rimuovi prima un voto esistente.', + 'notification.reason.sizeLimit': 'limite di dimensione', + 'notification.reason.unknownError': 'errore sconosciuto', + 'notification.reason.unsupportedFileType': 'tipo di file non supportato', + 'notification.replySearchFailed': 'Thread non trovato', + 'poll.actions.suggestOption.label': 'Suggerisci un’opzione', + 'poll.actions.viewComments.label_one': 'Vedi {{count}} commento', + 'poll.actions.viewComments.label_other': 'Vedi {{count}} commenti', + 'poll.actions.viewResults.label': 'Vedi i risultati', + 'poll.addCommentPrompt.addComment.label': 'Aggiungi un commento', + 'poll.addCommentPrompt.addCommentPollAnswer.label': + 'Aggiungi un commento alla tua risposta al sondaggio', + 'poll.addCommentPrompt.fieldCannotEmptyContain.label': + 'Questo campo non può essere vuoto né contenere solo spazi', + 'poll.addCommentPrompt.update.text': 'Aggiorna', + 'poll.addCommentPrompt.updateComment.label': 'Aggiorna il tuo commento', + 'poll.addCommentPrompt.updateCommentAttachedPoll.label': + 'Aggiorna il commento allegato alla tua risposta al sondaggio', + 'poll.answerList.description': 'Rivedi i commenti inviati con le risposte al sondaggio', + 'poll.answerList.pollComments.title': 'Commenti del sondaggio', + 'poll.creationDialog.allowOthersAddComments.description': + 'Consenti ad altri di aggiungere commenti', + 'poll.creationDialog.anonymousPoll.title': 'Sondaggio anonimo', + 'poll.creationDialog.createPoll.title': 'Crea sondaggio', + 'poll.creationDialog.hideWhoVoted.description': 'Nascondi chi ha votato', + 'poll.creationDialog.letOthersAddOptions.description': + 'Consenti ad altri di aggiungere opzioni', + 'poll.creationDialog.pollSent.text': 'Sondaggio inviato', + 'poll.creationDialog.sendPoll.text': 'Invia sondaggio', + 'poll.endPollAlert.description': + 'Vuoi terminare ora questo sondaggio? Nessuno potrà più votare.', + 'poll.endPollAlert.endPoll.text': 'Termina sondaggio', + 'poll.endPollAlert.endPoll.title': 'Terminare questo sondaggio?', + 'poll.header.selectOne.label': 'Seleziona una opzione', + 'poll.header.selectOneMore.label': 'Seleziona una o più opzioni', + 'poll.header.selectUp.label_one': 'Seleziona fino a {{count}}', + 'poll.header.selectUp.label_other': 'Seleziona fino a {{count}}', + 'poll.header.voteEnded.label': 'Votazione terminata', + 'poll.multipleAnswersField.chooseBetween210.description': 'Scegli da 2 a 10 opzioni', + 'poll.multipleAnswersField.enforceUniqueVoteEnabled.label': + 'L’obbligo di voto unico è attivo', + 'poll.multipleAnswersField.limitVotesPerPerson.title': 'Limita i voti per persona', + 'poll.multipleAnswersField.maximumVotesPerPerson.ariaLabel': 'Voti massimi per persona', + 'poll.multipleAnswersField.multipleVotes.title': 'Voti multipli', + 'poll.multipleAnswersField.onlyNumbersAllowed.label': 'Sono ammessi solo numeri', + 'poll.multipleAnswersField.selectMoreThanOne.description': + 'Seleziona più di una opzione', + 'poll.multipleAnswersField.typeNumber210.label': 'Inserisci un numero da 2 a 10', + 'poll.nameField.askQuestion.placeholder': 'Fai una domanda', + 'poll.nameField.error.text': 'Errore', + 'poll.nameField.questionRequired.label': 'La domanda è obbligatoria', + 'poll.optionFieldSet.addOption.placeholder': 'Aggiungi un’opzione', + 'poll.optionFieldSet.option.ariaLabel': 'Opzione {{ position }}', + 'poll.optionFieldSet.optionCanReorderedRemoved.ariaLabel': + 'Questa opzione può essere riordinata e rimossa.', + 'poll.optionFieldSet.optionEmpty.label': 'L’opzione è vuota', + 'poll.optionFieldSet.options.label': 'Opzioni', + 'poll.optionFieldSet.optionsCanNowReordered.ariaLabel': + 'Le opzioni possono ora essere riordinate e rimosse.', + 'poll.optionFieldSet.removeOption.ariaLabel': 'Rimuovi opzione: {{ option }}', + 'poll.optionList.moreOptions.label_one': '+{{count}} altra opzione', + 'poll.optionList.moreOptions.label_other': '+{{count}} altre opzioni', + 'poll.optionReorder.pressSpaceSelectOption.ariaLabel': + 'Premi Spazio per selezionare questa opzione, usa le frecce Su e Giù per spostarla, poi premi di nuovo Spazio per deselezionarla.', + 'poll.optionReorder.reorderOption.ariaLabel': 'Riordina l’opzione {{ position }}', + 'poll.optionReorder.reorderPosition.ariaLabel': + 'Riordina "{{ option }}" alla posizione {{ position }} di {{ total }}', + 'poll.optionVotes.question.text': 'Domanda {{ optionOrderNumber}}', + 'poll.optionVotes.view.text': 'Vedi tutti', + 'poll.optionVotes.votes.text_one': '{{count}} voto', + 'poll.optionVotes.votes.text_other': '{{count}} voti', + 'poll.optionsFull.description': + 'Rivedi tutte le opzioni disponibili in questo sondaggio', + 'poll.optionsFull.pollOptions.title': 'Opzioni del sondaggio', + 'poll.pollComment.placeholder': 'Il tuo commento', + 'poll.pollOptionSuggestion.placeholder': 'Inserisci una nuova opzione', + 'poll.question.question.text': 'Domanda', + 'poll.results.pollResults.title': 'Risultati del sondaggio', + 'poll.results.reviewPollResultsOpen.description': + 'Rivedi i risultati del sondaggio e apri un’opzione per vedere i voti in dettaglio', + 'poll.results.reviewWhoVotedOption.description': + 'Rivedi chi ha votato per questa opzione', + 'poll.results.totalVoteCount.text_one': '1 voto in totale', + 'poll.results.totalVoteCount.text_other': '{{ count }} voti in totale', + 'poll.results.votes.title': 'Voti', + 'poll.suggestPollOption.description': + 'Suggerisci una nuova opzione da aggiungere a questo sondaggio', + 'poll.suggestPollOption.optionAlreadyExists.label': 'L’opzione esiste già', + 'reactions.fetchReactions.errorFetchingReactions.text': + 'Errore durante il caricamento delle reazioni', + 'reactions.messageReactions.reactionList.ariaLabel': 'Elenco delle reazioni', + 'reactions.messageReactions.selectReaction.ariaLabel': + 'Seleziona reazione: {{ reactionName }}', + 'reactions.messageReactionsDetail.reactions.text_one': '{{ count }} reazione', + 'reactions.messageReactionsDetail.reactions.text_other': '{{ count }} reazioni', + 'reactions.messageReactionsDetail.tapRemove.ariaLabel': + 'Tocca per rimuovere: {{ reactionName }}', + 'reactions.messageReactionsDetail.tapRemove.text': 'Tocca per rimuovere', + 'relativeTime.daysAgo_one': '{{ count }}g fa', + 'relativeTime.daysAgo_other': '{{ count }}g fa', + 'relativeTime.today': 'Oggi', + 'relativeTime.weeksAgo_one': '{{ count }}sett fa', + 'relativeTime.weeksAgo_other': '{{ count }}sett fa', + 'relativeTime.yesterday': 'Ieri', + 'search.bar.clearSearch.ariaLabel': 'Cancella ricerca', + 'search.bar.exitSearch.ariaLabel': 'Esci dalla ricerca', + 'search.resultItem.selectUserChannel.ariaLabel': + 'Seleziona il canale utente: {{ name }}', + 'search.results.searchResults.ariaLabel': 'Risultati della ricerca', + 'search.resultsHeader.ariaLabel': 'Pulsante di filtro dei risultati per: {{ source }}', + 'search.resultsHeader.filterSource.channels': 'canali', + 'search.resultsHeader.filterSource.messages': 'messaggi', + 'search.resultsHeader.filterSource.users': 'utenti', + 'search.resultsPresearch.startTypingSearch.text': 'Inizia a digitare per cercare', + 'search.sourceResults.noResultsFound.text': 'Nessun risultato', + 'search.sourceResults.searching.text': 'Ricerca di {{ searchSourceType }}...', + 'slotLayout.chatView.channels.text': 'Canali', + 'slotLayout.chatView.chatViewControls.ariaLabel': 'Controlli della vista chat', + 'slotLayout.chatView.openChannelsView.ariaLabel': 'Apri la vista dei canali', + 'slotLayout.chatView.openThreadsView.ariaLabel': 'Apri la vista dei thread', + 'slotLayout.chatView.openThreadsViewUnread.ariaLabel_one': + 'Apri la vista dei thread, {{ count }} thread non letto', + 'slotLayout.chatView.openThreadsViewUnread.ariaLabel_other': + 'Apri la vista dei thread, {{ count }} thread non letti', + 'textareaComposer.messageInput.ariaLabel': 'Campo messaggio', + 'textareaComposer.roleItem.notifyMembers.label': 'Notifica tutti i membri {{ role }}', + 'textareaComposer.suggestionList.commandSuggestions.ariaLabel': + 'Suggerimenti di comandi', + 'textareaComposer.suggestionList.emojiSuggestions.ariaLabel': 'Suggerimenti di emoji', + 'textareaComposer.suggestionList.mentionSuggestions.ariaLabel': + 'Suggerimenti di menzioni', + 'textareaComposer.suggestionList.suggestions.ariaLabel': 'Suggerimenti', + 'textareaComposer.textareaPlaceholder.searchGiFs.label': 'Cerca GIF', + 'textareaComposer.textareaPlaceholder.sendMessage.label': 'Scrivi un messaggio', + 'textareaComposer.textareaPlaceholder.slowModeWaitS.label': + 'Modalità lenta, attendi {{ seconds }}s...', + 'thread.header.closeThread.ariaLabel': 'Chiudi thread', + 'thread.header.thread.text': 'Thread', + 'threadList.chat.ariaLabel': 'Chat: {{ channelName }}', + 'threadList.empty.text': 'Rispondi a un messaggio per iniziare un thread', + 'threadList.thread.ariaLabel': 'Thread: {{ messagePreview }}', + 'threadList.threadList.ariaLabel': 'Elenco dei thread', + 'threadList.unseenBanner.loading': 'Caricamento...', + 'threadList.unseenBanner.unreadThreads_one': '{{ count }} thread non letto', + 'threadList.unseenBanner.unreadThreads_other': '{{ count }} thread non letti', + 'typing.manyUsers_one': '{{ count }} persona sta scrivendo', + 'typing.manyUsers_other': '{{ count }} persone stanno scrivendo', + 'typing.singleUser': '{{ typing }} sta scrivendo', + 'typing.twoUsers': '{{ typing }} stanno scrivendo', + 'videoPlayer.videoThumbnail.playVideo.ariaLabel': 'Riproduci video', + 'timestamp.ChannelDetailPinnedMessageTimestamp': + '{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { "sameDay": "LT", "lastDay": "[Ieri]", "lastWeek": "dddd", "sameElse": "L" }) }}', + 'timestamp.ChannelPreviewTimestamp': + '{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { "sameDay": "LT", "lastDay": "[Ieri]", "lastWeek": "dddd", "sameElse": "L" }) }}', + 'timestamp.DateSeparator': + '{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { "sameDay": "[Oggi]", "nextDay": "[Domani]", "lastDay": "[Ieri]", "nextWeek": "dddd", "lastWeek": "[Lo scorso] dddd", "sameElse": "ddd D MMM" }) }}', + 'timestamp.ReminderNotification': + '{{ timestamp | timestampFormatter(calendar: true; calendarFormats: { "sameDay": "[Oggi] [alle] HH:mm", "nextDay": "[Domani] [alle] HH:mm", "lastDay": "[Ieri] [alle] HH:mm", "nextWeek": "dddd [alle] HH:mm", "lastWeek": "[Lo scorso] dddd [alle] HH:mm", "sameElse": "ddd D MMM [alle] HH:mm" }) }}', +} as const satisfies TranslationDictionary; + +/** + * Every key that is copy — the formatter expressions the SDK resolves from its own bundled + * defaults are not something an integrator translates. + */ +type TranslatableKey = Exclude< + keyof TranslationCatalog, + `duration.${string}` | `timestamp.${string}` | `translationBuilderTopic.${string}` +>; + +/** + * Compile-time completeness gate. `as const satisfies` above keeps the literal keys that + * `keyof typeof` needs while still checking each one against the catalog, so this resolves to + * `never` only when nothing is left untranslated. Add a key to the SDK without translating it here + * and the error names it. + */ +type Untranslated = Exclude; +type AssertNoneMissing = T; +export type ItalianIsComplete = AssertNoneMissing; + +/** + * Calendar wording for the keys that format against the locale's own calendar — + * `timestamp.LiveLocation` and `timestamp.PollVoteTooltip`. Passed as the third argument to + * `registerTranslation`, which is how a shared instance carries one config per language. + */ +export const itDayjsLocaleConfig = { + calendar: { + lastDay: '[ieri alle] LT', + lastWeek: '[lo scorso] dddd [alle] LT', + nextDay: '[domani alle] LT', + nextWeek: 'dddd [alle] LT', + sameDay: '[oggi alle] LT', + sameElse: 'L', + }, +}; diff --git a/src/i18n/Streami18n.ts b/src/i18n/Streami18n.ts index 0f77df3ede..218d180310 100644 --- a/src/i18n/Streami18n.ts +++ b/src/i18n/Streami18n.ts @@ -41,6 +41,15 @@ type CalendarLocaleConfig = { sameElse: string; }; +/** + * A dayjs locale config, as accepted by `dayjsLocaleConfigForLanguage` and by + * `registerTranslation`'s third argument. + * + * `calendar` is not part of dayjs's own `ILocale` — it comes from the calendar plugin — so it has to + * be added here. Supplying it is how relative wording ("heute um", "ieri alle") gets localized. + */ +export type DayjsLocaleConfig = Partial & { calendar?: CalendarLocaleConfig }; + Dayjs.extend(updateLocale); Dayjs.extend(utc); Dayjs.extend(timezone); @@ -86,7 +95,7 @@ const supportsTz = (dateTimeParser: unknown): dateTimeParser is TimezoneParser = export type Streami18nOptions = { DateTimeParser?: DateTimeParserModule; - dayjsLocaleConfigForLanguage?: Partial & { calendar?: CalendarLocaleConfig }; + dayjsLocaleConfigForLanguage?: DayjsLocaleConfig; debug?: boolean; disableDateTimeTranslations?: boolean; formatters?: Partial & CustomFormatters; @@ -237,7 +246,7 @@ export class Streami18n { * given to registerTranslation() function in `dayjsLocales` object, and register the required locale * with moment, when setLanguage is called. * */ - dayjsLocales: { [key: string]: Partial } = {}; + dayjsLocales: { [key: string]: DayjsLocaleConfig } = {}; // dayjsLocales = {}; /** @@ -505,7 +514,7 @@ export class Streami18n { registerTranslation( language: string, translation: TranslationDictionary, - customDayjsLocale?: Partial, + customDayjsLocale?: DayjsLocaleConfig, ) { // Merged, not replaced, so repeated calls for one language accumulate. const merged = this.mergeWithRuntimeDefaults(language, translation); @@ -530,7 +539,7 @@ export class Streami18n { } } - addOrUpdateLocale(key: string, config: Partial) { + addOrUpdateLocale(key: string, config: DayjsLocaleConfig) { if (this.localeExists(key)) { Dayjs.updateLocale(key, { ...config }); } else { diff --git a/yarn.lock b/yarn.lock index 18ab1587dd..2b30f5dadc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1849,6 +1849,7 @@ __metadata: "@vitejs/plugin-react-swc": "npm:^4.3.1" babel-plugin-react-compiler: "npm:^1.0.0" clsx: "npm:^2.1.1" + dayjs: "npm:^1.11.20" emoji-mart: "npm:^5.6.0" human-id: "npm:^4.1.3" modern-normalize: "npm:^3.0.1"