From eecf782737eaef3f746e7e2f2a8af0c7619bc56d Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 10 Jan 2024 16:19:49 -0300 Subject: [PATCH] refactor(i18n): Translation's lint and load (#31343) --- apps/meteor/.scripts/check-i18n.js | 109 -------- apps/meteor/.scripts/translation-check.ts | 249 ++++++++++++++++++ ...translationDiff.js => translation-diff.ts} | 21 +- .../{fix-i18n.js => translation-fix-order.ts} | 6 +- apps/meteor/app/utils/lib/i18n.ts | 121 +++++++++ .../lib/utils/applyCustomTranslations.ts | 28 -- .../client/providers/TranslationProvider.tsx | 155 ++++------- apps/meteor/package.json | 8 +- .../rocketchat-i18n/i18n/ar.i18n.json | 2 +- .../rocketchat-i18n/i18n/ca.i18n.json | 2 +- .../rocketchat-i18n/i18n/cs.i18n.json | 2 +- .../rocketchat-i18n/i18n/da.i18n.json | 2 +- .../rocketchat-i18n/i18n/de-IN.i18n.json | 2 +- .../rocketchat-i18n/i18n/de.i18n.json | 2 +- .../rocketchat-i18n/i18n/en.i18n.json | 4 +- .../rocketchat-i18n/i18n/es.i18n.json | 4 +- .../rocketchat-i18n/i18n/fi.i18n.json | 6 +- .../rocketchat-i18n/i18n/fr.i18n.json | 2 +- .../rocketchat-i18n/i18n/hu.i18n.json | 2 +- .../rocketchat-i18n/i18n/it.i18n.json | 2 +- .../rocketchat-i18n/i18n/ja.i18n.json | 4 +- .../rocketchat-i18n/i18n/ka-GE.i18n.json | 10 +- .../rocketchat-i18n/i18n/km.i18n.json | 2 +- .../rocketchat-i18n/i18n/ko.i18n.json | 2 +- .../rocketchat-i18n/i18n/mn.i18n.json | 6 +- .../rocketchat-i18n/i18n/ms-MY.i18n.json | 2 +- .../rocketchat-i18n/i18n/nl.i18n.json | 2 +- .../rocketchat-i18n/i18n/pl.i18n.json | 6 +- .../rocketchat-i18n/i18n/pt-BR.i18n.json | 2 +- .../rocketchat-i18n/i18n/pt.i18n.json | 2 +- .../rocketchat-i18n/i18n/ru.i18n.json | 6 +- .../rocketchat-i18n/i18n/sv.i18n.json | 6 +- .../rocketchat-i18n/i18n/ta-IN.i18n.json | 2 +- .../rocketchat-i18n/i18n/tr.i18n.json | 2 +- .../rocketchat-i18n/i18n/zh-TW.i18n.json | 2 +- .../rocketchat-i18n/i18n/zh.i18n.json | 2 +- apps/meteor/server/lib/i18n.ts | 15 +- apps/meteor/server/settings/email.ts | 2 +- packages/i18n/src/index.mjs | 6 +- packages/ui-contexts/package.json | 2 + .../ui-contexts/src/TranslationContext.ts | 7 +- packages/ui-contexts/src/en.json | 1 - yarn.lock | 13 +- 43 files changed, 515 insertions(+), 318 deletions(-) delete mode 100644 apps/meteor/.scripts/check-i18n.js create mode 100644 apps/meteor/.scripts/translation-check.ts rename apps/meteor/.scripts/{translationDiff.js => translation-diff.ts} (54%) rename apps/meteor/.scripts/{fix-i18n.js => translation-fix-order.ts} (84%) delete mode 100644 apps/meteor/client/lib/utils/applyCustomTranslations.ts delete mode 120000 packages/ui-contexts/src/en.json diff --git a/apps/meteor/.scripts/check-i18n.js b/apps/meteor/.scripts/check-i18n.js deleted file mode 100644 index a56980f2406a..000000000000 --- a/apps/meteor/.scripts/check-i18n.js +++ /dev/null @@ -1,109 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const fg = require('fast-glob'); - -const regexVar = /__[a-zA-Z_]+__/g; - -const validateKeys = (json, usedKeys) => - usedKeys - .filter(({ key }) => typeof json[key] !== 'undefined') - .reduce((prev, cur) => { - const { key, replaces } = cur; - - const miss = replaces.filter((replace) => json[key] && json[key].indexOf(replace) === -1); - - if (miss.length > 0) { - prev.push({ key, miss }); - } - - return prev; - }, []); - -const removeMissingKeys = (i18nFiles, usedKeys) => { - i18nFiles.forEach((file) => { - const json = JSON.parse(fs.readFileSync(file, 'utf8')); - if (Object.keys(json).length === 0) { - return; - } - - validateKeys(json, usedKeys).forEach(({ key }) => { - json[key] = null; - }); - - fs.writeFileSync(file, JSON.stringify(json, null, 2)); - }); -}; - -const checkUniqueKeys = (content, json, filename) => { - const matchKeys = content.matchAll(/^\s+"([^"]+)"/gm); - - const allKeys = [...matchKeys]; - - if (allKeys.length !== Object.keys(json).length) { - throw new Error(`Duplicated keys found on file ${filename}`); - } -}; - -const validate = (i18nFiles, usedKeys) => { - const totalErrors = i18nFiles.reduce((errors, file) => { - const content = fs.readFileSync(file, 'utf8'); - const json = JSON.parse(content); - - checkUniqueKeys(content, json, file); - - // console.log('json, usedKeys2', json, usedKeys); - - const result = validateKeys(json, usedKeys); - - if (result.length === 0) { - return errors; - } - - console.log('\n## File', file, `(${result.length} errors)`); - - result.forEach(({ key, miss }) => { - console.log('\n- Key:', key, '\n Missing variables:', miss.join(', ')); - }); - - return errors + result.length; - }, 0); - - if (totalErrors > 0) { - throw new Error(`\n${totalErrors} errors found`); - } -}; - -const checkFiles = async (sourcePath, sourceFile, fix = false) => { - const content = fs.readFileSync(path.join(sourcePath, sourceFile), 'utf8'); - const sourceContent = JSON.parse(content); - - checkUniqueKeys(content, sourceContent, sourceFile); - - const usedKeys = Object.entries(sourceContent).map(([key, value]) => { - const replaces = value.match(regexVar); - return { - key, - replaces, - }; - }); - - const keysWithInterpolation = usedKeys.filter(({ replaces }) => !!replaces); - - const i18nFiles = await fg([`${sourcePath}/**/*.i18n.json`]); - - if (fix) { - return removeMissingKeys(i18nFiles, keysWithInterpolation); - } - - validate(i18nFiles, keysWithInterpolation); -}; - -(async () => { - try { - await checkFiles('./packages/rocketchat-i18n/i18n', 'en.i18n.json', process.argv[2] === '--fix'); - } catch (e) { - console.error(e); - process.exit(1); - } -})(); diff --git a/apps/meteor/.scripts/translation-check.ts b/apps/meteor/.scripts/translation-check.ts new file mode 100644 index 000000000000..0a1b9d319b2c --- /dev/null +++ b/apps/meteor/.scripts/translation-check.ts @@ -0,0 +1,249 @@ +import type { PathLike } from 'node:fs'; +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { inspect } from 'node:util'; + +import fg from 'fast-glob'; +import i18next from 'i18next'; +import supportsColor from 'supports-color'; + +const hasDuplicatedKeys = (content: string, json: Record) => { + const matchKeys = content.matchAll(/^\s+"([^"]+)"/gm); + + const allKeys = [...matchKeys]; + + return allKeys.length !== Object.keys(json).length; +}; + +const parseFile = async (path: PathLike) => { + const content = await readFile(path, 'utf-8'); + let json: Record; + try { + json = JSON.parse(content); + } catch (e) { + if (e instanceof SyntaxError) { + const matches = /^Unexpected token .* in JSON at position (\d+)$/.exec(e.message); + + if (matches) { + const [, positionStr] = matches; + const position = parseInt(positionStr, 10); + const line = content.slice(0, position).split('\n').length; + const column = position - content.slice(0, position).lastIndexOf('\n'); + throw new SyntaxError(`Invalid JSON on file ${path}:${line}:${column}`); + } + } + throw new SyntaxError(`Invalid JSON on file ${path}: ${e.message}`); + } + + if (hasDuplicatedKeys(content, json)) { + throw new SyntaxError(`Duplicated keys found on file ${path}`); + } + + return json; +}; + +const insertTranslation = (json: Record, refKey: string, [key, value]: [key: string, value: string]) => { + const entries = Object.entries(json); + + const refIndex = entries.findIndex(([entryKey]) => entryKey === refKey); + + if (refIndex === -1) { + throw new Error(`Reference key ${refKey} not found`); + } + + const movingEntries = entries.slice(refIndex + 1); + + for (const [key] of movingEntries) { + delete json[key]; + } + + json[key] = value; + + for (const [key, value] of movingEntries) { + json[key] = value; + } +}; + +const persistFile = async (path: PathLike, json: Record) => { + const content = JSON.stringify(json, null, 2); + + await writeFile(path, content, 'utf-8'); +}; + +const oldPlaceholderFormat = /__([a-zA-Z_]+)__/g; + +const checkPlaceholdersFormat = async ({ json, path, fix = false }: { json: Record; path: PathLike; fix?: boolean }) => { + const outdatedKeys = Object.entries(json) + .map(([key, value]) => ({ + key, + value, + placeholders: value.match(oldPlaceholderFormat), + })) + .filter((outdatedKey): outdatedKey is { key: string; value: string; placeholders: RegExpMatchArray } => !!outdatedKey.placeholders); + + if (outdatedKeys.length > 0) { + const message = `Outdated placeholder format on file ${path}: ${inspect(outdatedKeys, { colors: !!supportsColor.stdout })}`; + + if (fix) { + console.warn(message); + + for (const { key, value } of outdatedKeys) { + const newValue = value.replace(oldPlaceholderFormat, (_, name) => `{{${name}}}`); + + json[key] = newValue; + } + + await persistFile(path, json); + + return; + } + + throw new Error(message); + } +}; + +export const extractSingularKeys = (json: Record, lng: string) => { + if (!i18next.isInitialized) { + i18next.init({ initImmediate: false }); + } + + const pluralSuffixes = i18next.services.pluralResolver.getSuffixes(lng) as string[]; + + const singularKeys = new Set( + Object.keys(json).map((key) => { + for (const pluralSuffix of pluralSuffixes) { + if (key.endsWith(pluralSuffix)) { + return key.slice(0, -pluralSuffix.length); + } + } + + return key; + }), + ); + + return [singularKeys, pluralSuffixes] as const; +}; + +const checkMissingPlurals = async ({ + json, + path, + lng, + fix = false, +}: { + json: Record; + path: PathLike; + lng: string; + fix?: boolean; +}) => { + const [singularKeys, pluralSuffixes] = extractSingularKeys(json, lng); + + const missingPluralKeys: { singularKey: string; existing: string[]; missing: string[] }[] = []; + + for (const singularKey of singularKeys) { + if (singularKey in json) { + continue; + } + + const pluralKeys = pluralSuffixes.map((suffix) => `${singularKey}${suffix}`); + + const existing = pluralKeys.filter((key) => key in json); + const missing = pluralKeys.filter((key) => !(key in json)); + + if (missing.length > 0) { + missingPluralKeys.push({ singularKey, existing, missing }); + } + } + + if (missingPluralKeys.length > 0) { + const message = `Missing plural keys on file ${path}: ${inspect(missingPluralKeys, { colors: !!supportsColor.stdout })}`; + + if (fix) { + console.warn(message); + + for (const { existing, missing } of missingPluralKeys) { + for (const missingKey of missing) { + const refKey = existing.slice(-1)[0]; + const value = json[refKey]; + insertTranslation(json, refKey, [missingKey, value]); + } + } + + await persistFile(path, json); + + return; + } + + throw new Error(message); + } +}; + +const checkExceedingKeys = async ({ + json, + path, + lng, + sourceJson, + sourceLng, + fix = false, +}: { + json: Record; + path: PathLike; + lng: string; + sourceJson: Record; + sourceLng: string; + fix?: boolean; +}) => { + const [singularKeys] = extractSingularKeys(json, lng); + const [sourceSingularKeys] = extractSingularKeys(sourceJson, sourceLng); + + const exceedingKeys = [...singularKeys].filter((key) => !sourceSingularKeys.has(key)); + + if (exceedingKeys.length > 0) { + const message = `Exceeding keys on file ${path}: ${inspect(exceedingKeys, { colors: !!supportsColor.stdout })}`; + + if (fix) { + for (const key of exceedingKeys) { + delete json[key]; + } + + await persistFile(path, json); + + return; + } + + throw new Error(message); + } +}; + +const checkFiles = async (sourceDirPath: string, sourceLng: string, fix = false) => { + const sourcePath = join(sourceDirPath, `${sourceLng}.i18n.json`); + const sourceJson = await parseFile(sourcePath); + + await checkPlaceholdersFormat({ json: sourceJson, path: sourcePath, fix }); + await checkMissingPlurals({ json: sourceJson, path: sourcePath, lng: sourceLng, fix }); + + const i18nFiles = await fg([join(sourceDirPath, `**/*.i18n.json`), `!${sourcePath}`]); + + const languageFileRegex = /\/([^\/]*?).i18n.json$/; + const translations = await Promise.all( + i18nFiles.map(async (path) => { + const lng = languageFileRegex.exec(path)?.[1]; + if (!lng) { + throw new Error(`Invalid language file path ${path}`); + } + + return { path, json: await parseFile(path), lng }; + }), + ); + + for await (const { path, json, lng } of translations) { + await checkPlaceholdersFormat({ json, path, fix }); + await checkMissingPlurals({ json, path, lng, fix }); + await checkExceedingKeys({ json, path, lng, sourceJson, sourceLng, fix }); + } +}; + +const fix = process.argv[2] === '--fix'; +checkFiles('./packages/rocketchat-i18n/i18n', 'en', fix).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/apps/meteor/.scripts/translationDiff.js b/apps/meteor/.scripts/translation-diff.ts similarity index 54% rename from apps/meteor/.scripts/translationDiff.js rename to apps/meteor/.scripts/translation-diff.ts index 7c83e33c76ee..0ee7a1c72b9d 100644 --- a/apps/meteor/.scripts/translationDiff.js +++ b/apps/meteor/.scripts/translation-diff.ts @@ -1,18 +1,14 @@ -#!/usr/bin/env node +#!/usr/bin/env ts-node -const fs = require('fs'); -const path = require('path'); -const util = require('util'); - -// Convert fs.readFile into Promise version of same -const readFile = util.promisify(fs.readFile); +import { readFile } from 'fs/promises'; +import path from 'path'; const translationDir = path.resolve(__dirname, '../packages/rocketchat-i18n/i18n/'); -async function translationDiff(source, target) { +async function translationDiff(source: string, target: string) { console.debug('loading translations from', translationDir); - function diffKeys(a, b) { + function diffKeys(a: Record, b: Record) { const diff = {}; Object.keys(a).forEach((key) => { if (!b[key]) { @@ -29,10 +25,9 @@ async function translationDiff(source, target) { return diffKeys(sourceTranslations, targetTranslations); } -console.log('Note: You can set the source and target language of the comparison with env-variables SOURCE/TARGET_LANGUAGE'); -const sourceLang = process.env.SOURCE_LANGUAGE || 'en'; -const targetLang = process.env.TARGET_LANGUAGE || 'de'; +const sourceLang = process.argv[2] || 'en'; +const targetLang = process.argv[3] || 'de'; translationDiff(sourceLang, targetLang).then((diff) => { console.log('Diff between', sourceLang, 'and', targetLang); - console.log(JSON.stringify(diff, '', 2)); + console.log(JSON.stringify(diff, undefined, 2)); }); diff --git a/apps/meteor/.scripts/fix-i18n.js b/apps/meteor/.scripts/translation-fix-order.ts similarity index 84% rename from apps/meteor/.scripts/fix-i18n.js rename to apps/meteor/.scripts/translation-fix-order.ts index f0002c8ca4eb..14eba2e73682 100644 --- a/apps/meteor/.scripts/fix-i18n.js +++ b/apps/meteor/.scripts/translation-fix-order.ts @@ -6,11 +6,11 @@ * - remove all keys not present in source i18n file */ -const fs = require('fs'); +import fs from 'fs'; -const fg = require('fast-glob'); +import fg from 'fast-glob'; -const fixFiles = (path, source, newlineAtEnd = false) => { +const fixFiles = (path: string, source: string, newlineAtEnd = false) => { const sourceFile = JSON.parse(fs.readFileSync(`${path}${source}`, 'utf8')); const sourceKeys = Object.keys(sourceFile); diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index a491159e49e9..efbfa15cefa6 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -1,3 +1,4 @@ +import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import i18next from 'i18next'; import sprintf from 'i18next-sprintf-postprocessor'; @@ -19,3 +20,123 @@ export const addSprinfToI18n = function (t: (key: string, ...replaces: any) => s }; export const t = addSprinfToI18n(i18n.t.bind(i18n)); + +/** + * Extract the translation keys from a flat object and group them by namespace + * + * Example: + * + * ```js + * const source = { + * 'core.key1': 'value1', + * 'core.key2': 'value2', + * 'onboarding.key1': 'value1', + * 'onboarding.key2': 'value2', + * 'registration.key1': 'value1', + * 'registration.key2': 'value2', + * 'cloud.key1': 'value1', + * 'cloud.key2': 'value2', + * 'subscription.key1': 'value1', + * 'subscription.key2': 'value2', + * }; + * + * const result = extractTranslationNamespaces(source); + * + * console.log(result); + * + * // { + * // core: { + * // key1: 'value1', + * // key2: 'value2' + * // }, + * // onboarding: { + * // key1: 'value1', + * // key2: 'value2' + * // }, + * // registration: { + * // key1: 'value1', + * // key2: 'value2' + * // }, + * // cloud: { + * // key1: 'value1', + * // key2: 'value2' + * // }, + * // subscription: { + * // key1: 'value1', + * // key2: 'value2' + * // } + * // } + * ``` + * + * @param source the flat object with the translation keys + */ +export const extractTranslationNamespaces = (source: Record): Record> => { + const result: Record> = { + core: {}, + onboarding: {}, + registration: {}, + cloud: {}, + subscription: {}, + }; + + for (const [key, value] of Object.entries(source)) { + const prefix = availableTranslationNamespaces.find((namespace) => key.startsWith(`${namespace}.`)); + const keyWithoutNamespace = prefix ? key.slice(prefix.length + 1) : key; + const ns = prefix ?? defaultTranslationNamespace; + result[ns][keyWithoutNamespace] = value; + } + + return result; +}; + +/** + * Extract only the translation keys that match the given namespaces + * + * @param source the flat object with the translation keys + * @param namespaces the namespaces to extract + */ +export const extractTranslationKeys = (source: Record, namespaces: string | string[] = []): { [key: string]: any } => { + const all = extractTranslationNamespaces(source); + return Array.isArray(namespaces) + ? (namespaces as TranslationNamespace[]).reduce((result, namespace) => ({ ...result, ...all[namespace] }), {}) + : all[namespaces as TranslationNamespace]; +}; + +export type TranslationNamespace = + | (Extract extends `${infer T}.${string}` ? (T extends Lowercase ? T : never) : never) + | 'core'; + +const namespacesMap: Record = { + core: true, + onboarding: true, + registration: true, + cloud: true, + subscription: true, +}; + +export const availableTranslationNamespaces = Object.keys(namespacesMap) as TranslationNamespace[]; +export const defaultTranslationNamespace: TranslationNamespace = 'core'; + +export const applyCustomTranslations = ( + i18n: typeof i18next, + parsedCustomTranslations: Record>, + { namespaces, languages }: { namespaces?: string[]; languages?: string[] } = {}, +) => { + for (const [lng, translations] of Object.entries(parsedCustomTranslations)) { + if (languages && !languages.includes(lng)) { + continue; + } + + for (const [key, value] of Object.entries(translations)) { + const prefix = availableTranslationNamespaces.find((namespace) => key.startsWith(`${namespace}.`)); + const keyWithoutNamespace = prefix ? key.slice(prefix.length + 1) : key; + const ns = prefix ?? defaultTranslationNamespace; + + if (namespaces && !namespaces.includes(ns)) { + continue; + } + + i18n.addResourceBundle(lng, ns, { [keyWithoutNamespace]: value }, true, true); + } + } +}; diff --git a/apps/meteor/client/lib/utils/applyCustomTranslations.ts b/apps/meteor/client/lib/utils/applyCustomTranslations.ts deleted file mode 100644 index f629ed1aaace..000000000000 --- a/apps/meteor/client/lib/utils/applyCustomTranslations.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { settings } from '../../../app/settings/client'; -import { i18n } from '../../../app/utils/lib/i18n'; - -const parseToJSON = (customTranslations: string) => { - try { - return JSON.parse(customTranslations); - } catch (e) { - return false; - } -}; - -export const applyCustomTranslations = (): void => { - const customTranslations: string | undefined = settings.get('Custom_Translations'); - - if (!customTranslations || !parseToJSON(customTranslations)) { - return; - } - - try { - const parsedCustomTranslations: Record = JSON.parse(customTranslations); - - for (const [lang, translations] of Object.entries(parsedCustomTranslations)) { - i18n.addResourceBundle(lang, 'core', translations); - } - } catch (e) { - console.error('Invalid setting Custom_Translations', e); - } -}; diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index f9fdf299a5d6..7f98c374f949 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -1,8 +1,8 @@ -import { useLocalStorage, useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; import languages from '@rocket.chat/i18n/dist/languages'; import en from '@rocket.chat/i18n/src/locales/en.i18n.json'; import { normalizeLanguage } from '@rocket.chat/tools'; -import type { TranslationKey, TranslationContextValue } from '@rocket.chat/ui-contexts'; +import type { TranslationContextValue } from '@rocket.chat/ui-contexts'; import { useMethod, useSetting, TranslationContext } from '@rocket.chat/ui-contexts'; import type i18next from 'i18next'; import I18NextHttpBackend from 'i18next-http-backend'; @@ -14,99 +14,73 @@ import { I18nextProvider, initReactI18next, useTranslation } from 'react-i18next import { CachedCollectionManager } from '../../app/ui-cached-collection/client'; import { getURL } from '../../app/utils/client'; -import { i18n, addSprinfToI18n } from '../../app/utils/lib/i18n'; +import { + i18n, + addSprinfToI18n, + extractTranslationKeys, + applyCustomTranslations, + availableTranslationNamespaces, + defaultTranslationNamespace, + extractTranslationNamespaces, +} from '../../app/utils/lib/i18n'; import { AppClientOrchestratorInstance } from '../../ee/client/apps/orchestrator'; -import { applyCustomTranslations } from '../lib/utils/applyCustomTranslations'; import { isRTLScriptLanguage } from '../lib/utils/isRTLScriptLanguage'; i18n.use(I18NextHttpBackend).use(initReactI18next).use(sprintf); -type TranslationNamespace = Extract extends `${infer T}.${string}` - ? T extends Lowercase - ? T - : never - : never; - -const namespacesDefault = ['core', 'onboarding', 'registration', 'cloud'] as TranslationNamespace[]; - -const parseToJSON = (customTranslations: string): Record> | false => { - try { - return JSON.parse(customTranslations); - } catch (e) { - return false; - } -}; - -const localeCache = new Map>(); - -const useI18next = (lng: string): typeof i18next => { +const useCustomTranslations = (i18n: typeof i18next) => { const customTranslations = useSetting('Custom_Translations'); - const parsedCustomTranslations = useMemo(() => { + const parsedCustomTranslations = useMemo((): Record> | undefined => { if (!customTranslations || typeof customTranslations !== 'string') { - return; + return undefined; } - return parseToJSON(customTranslations); + try { + return JSON.parse(customTranslations); + } catch (e) { + console.error(e); + return undefined; + } }, [customTranslations]); - const extractKeys = useMutableCallback( - (source: Record, lngs?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => { - const result: { [key: string]: any } = {}; - - for (const [key, value] of Object.entries(source)) { - const [prefix] = key.split('.'); - - if (prefix && Array.isArray(namespaces) ? namespaces.includes(prefix) : prefix === namespaces) { - result[key.slice(prefix.length + 1)] = value; - continue; - } - - if (Array.isArray(namespaces) ? namespaces.includes('core') : namespaces === 'core') { - result[key] = value; - } - } + useEffect(() => { + if (!parsedCustomTranslations) { + return; + } - if (lngs && parsedCustomTranslations) { - for (const language of Array.isArray(lngs) ? lngs : [lngs]) { - if (!parsedCustomTranslations[language]) { - continue; - } + applyCustomTranslations(i18n, parsedCustomTranslations); - for (const [key, value] of Object.entries(parsedCustomTranslations[language])) { - const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); + const handleLanguageChanged = (): void => { + applyCustomTranslations(i18n, parsedCustomTranslations); + }; - if (prefix) { - result[key.slice(prefix.length + 1)] = value; - continue; - } + i18n.on('languageChanged', handleLanguageChanged); - if (Array.isArray(namespaces) ? namespaces.includes('core') : namespaces === 'core') { - result[key] = value; - } - } - } - } + return () => { + i18n.off('languageChanged', handleLanguageChanged); + }; + }, [i18n, parsedCustomTranslations]); +}; - return result; - }, - ); +const localeCache = new Map>(); +const useI18next = (lng: string): typeof i18next => { if (!i18n.isInitialized) { i18n.init({ lng, fallbackLng: 'en', - ns: namespacesDefault, + ns: availableTranslationNamespaces, + defaultNS: defaultTranslationNamespace, nsSeparator: '.', resources: { - en: extractKeys(en), + en: extractTranslationNamespaces(en), }, partialBundledLanguages: true, - defaultNS: 'core', backend: { loadPath: 'i18n/{{lng}}.json', - parse: (data: string, lngs?: string | string[], namespaces: string | string[] = []) => - extractKeys(JSON.parse(data), lngs, namespaces), + parse: (data: string, _lngs?: string | string[], namespaces: string | string[] = []) => + extractTranslationKeys(JSON.parse(data), namespaces), request: (_options, url, _payload, callback) => { const params = url.split('/'); @@ -137,47 +111,12 @@ const useI18next = (lng: string): typeof i18next => { } useEffect(() => { - if (i18n.language !== lng) { - i18n.changeLanguage(lng); - } + i18n.changeLanguage(lng); }, [lng]); - useEffect(() => { - if (!parsedCustomTranslations) { - return; - } - - for (const [ln, translations] of Object.entries(parsedCustomTranslations)) { - if (!translations) { - continue; - } - const namespaces = Object.entries(translations).reduce((acc, [key, value]): Record> => { - const namespace = key.split('.')[0]; - - if (namespacesDefault.includes(namespace as unknown as TranslationNamespace)) { - acc[namespace] = acc[namespace] ?? {}; - acc[namespace][key] = value; - acc[namespace][key.slice(namespace.length + 1)] = value; - return acc; - } - acc.project = acc.project ?? {}; - acc.project[key] = value; - return acc; - }, {} as Record>); - - for (const [namespace, translations] of Object.entries(namespaces)) { - i18n.addResourceBundle(ln, namespace, translations); - } - } - }, [parsedCustomTranslations]); - return i18n; }; -type TranslationProviderProps = { - children: ReactNode; -}; - const useAutoLanguage = () => { const serverLanguage = useSetting('Language'); const browserLanguage = normalizeLanguage(window.navigator.userLanguage ?? window.navigator.language); @@ -206,11 +145,17 @@ const getLanguageName = (code: string, lng: string): string => { } }; +type TranslationProviderProps = { + children: ReactNode; +}; + const TranslationProvider = ({ children }: TranslationProviderProps): ReactElement => { const loadLocale = useMethod('loadLocale'); const language = useAutoLanguage(); const i18nextInstance = useI18next(language); + useCustomTranslations(i18nextInstance); + const availableLanguages = useMemo( () => [ { @@ -290,8 +235,8 @@ const TranslationProviderInner = ({ () => ({ language: i18n.language, languages: availableLanguages, - loadLanguage: async (language: string): Promise => { - i18n.changeLanguage(language).then(() => applyCustomTranslations()); + loadLanguage: async (language: string) => { + i18n.changeLanguage(language); }, translate: Object.assign(addSprinfToI18n(t), { has: ((key, options) => key && i18n.exists(key, options)) as TranslationContextValue['translate']['has'], diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 2db612565917..e63b4bb7c71f 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -44,9 +44,9 @@ ".testunit:definition": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --config ./.mocharc.definition.js", "testunit-watch": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' mocha --watch --config ./.mocharc.js", "test": "npm run testapi && npm run testui", - "translation-diff": "node .scripts/translationDiff.js", - "translation-check": "node .scripts/check-i18n.js", - "translation-fix-order": "node .scripts/fix-i18n.js", + "translation-diff": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-diff.ts", + "translation-check": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-check.ts", + "translation-fix-order": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-fix-order.ts", "version": "node .scripts/version.js", "set-version": "node .scripts/set-version.js", "release": "meteor npm run set-version --silent", @@ -148,6 +148,7 @@ "@types/strict-uri-encode": "^2.0.1", "@types/string-strip-html": "^5.0.1", "@types/supertest": "^2.0.15", + "@types/supports-color": "~7.2.0", "@types/textarea-caret": "^3.0.2", "@types/ua-parser-js": "^0.7.38", "@types/use-subscription": "^1.0.1", @@ -204,6 +205,7 @@ "stylelint": "^14.9.1", "stylelint-order": "^5.0.0", "supertest": "^6.2.3", + "supports-color": "~7.2.0", "template-file": "^6.0.1", "ts-node": "^10.9.1", "typescript": "~5.3.2" diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index f4edf2fed22f..6060705de4d3 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -2575,7 +2575,7 @@ "leave-c_description": "إذن لمغادرة القنوات", "leave-p": "مغادرة المجموعات الخاصة", "leave-p_description": "إذن لمغادرة المجموعات الخاصة", - "Lets_get_you_new_one": "دعنا نحضر لك واحدة جديدة!", + "Lets_get_you_new_one_": "دعنا نحضر لك واحدة جديدة!", "Link_Preview": "رابط المعاينة", "List_of_Channels": "قائمة Channels", "List_of_departments_for_forward": "قائمة الأقسام المسموح بإعادة توجيهها (اختياري)", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index b56b0b645d6b..f61d28735fe6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -2549,7 +2549,7 @@ "leave-c_description": "Permís per sortir de canals", "leave-p": "Sortir de grups privats", "leave-p_description": "Permís per sortir de grups privats", - "Lets_get_you_new_one": "Et portem un de nou!", + "Lets_get_you_new_one_": "Et portem un de nou!", "List_of_Channels": "Llista de canals", "List_of_departments_for_forward": "Llista de departaments permesos per reenviament (opcional)", "List_of_departments_for_forward_description": "Permetre establir una llista restringida de departaments que poden rebre xats d'aquest departament", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index ef36a4dbb928..1c5931521c91 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -2169,7 +2169,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Pokud nechcete zobrazovat roli, ponechte pole popisu prázdné", "leave-c": "Odejít z místností", "leave-p": "Opustit soukromé skupiny", - "Lets_get_you_new_one": "Pojďme si pořídit nový!", + "Lets_get_you_new_one_": "Pojďme si pořídit nový!", "List_of_Channels": "Seznam místností", "List_of_departments_for_forward": "Seznam oddělení povolených pro přesměrování (volitelné)", "List_of_departments_for_forward_description": "Omezit oddělení do kterých je možné přesměrovat konverzace z aktuálního", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index 4bf7e41fb9e1..d9aaf3f0a003 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -2180,7 +2180,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Lad beskrivelsesfeltet være tomt, hvis du ikke vil vise rollen", "leave-c": "Forlad kanaler", "leave-p": "Forlad private grupper", - "Lets_get_you_new_one": "Lad os finde en ny til dig!", + "Lets_get_you_new_one_": "Lad os finde en ny til dig!", "List_of_Channels": "Liste over kanaler", "List_of_departments_for_forward": "Liste over tilladte afdelinger til videresendelse (valgfrit)", "List_of_departments_for_forward_description": "Tillad at indstille en begrænset liste over afdelinger der kan modtage chats fra denne afdeling", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json index 5947c8517e54..2248f5ef3022 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json @@ -1767,7 +1767,7 @@ "Leave_the_current_channel": "Aktuellen Kanal verlassen", "leave-c": "Kanäle verlassen", "leave-p": "Verlasse private Gruppen", - "Lets_get_you_new_one": "Lass mich Ihnen ein neues geben!", + "Lets_get_you_new_one_": "Lass mich Ihnen ein neues geben!", "List_of_Channels": "Liste der Kanäle", "List_of_Direct_Messages": "Liste der Direktnachrichten", "Livechat": "Livechat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index de81f4a1a00f..68c54237e99b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -2863,7 +2863,7 @@ "leave-c_description": "Berechtigung, Channels zu verlassen", "leave-p": "Private Gruppen verlassen", "leave-p_description": "Erlaubnis, private Gruppen zu verlassen", - "Lets_get_you_new_one": "Geben wir Ihnen ein neues!", + "Lets_get_you_new_one_": "Geben wir Ihnen ein neues!", "License": "Lizenz", "Link_Preview": "Link-Vorschau", "List_of_Channels": "Liste der Channels", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 79bdfbf74a0f..e4b947b0a047 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3082,7 +3082,7 @@ "leave-c_description": "Permission to leave channels", "leave-p": "Leave Private Groups", "leave-p_description": "Permission to leave private groups", - "Lets_get_you_new_one": "Let's get you a new one!", + "Lets_get_you_new_one_": "Let's get you a new one!", "Let_them_know": "Let them know", "License": "License", "Line": "Line", @@ -5891,7 +5891,7 @@ "Your_password_is_wrong": "Your password is wrong!", "Your_password_was_changed_by_an_admin": "Your password was changed by an admin.", "Your_push_was_sent_to_s_devices": "Your push was sent to %s devices", - "Your_request_to_join__roomName__has_been_made_it_could_take_up_to_15_minutes_to_be_processed": "Your request to join __roomName__ has been made, it could take up to 15 minutes to be processed. You'll be notified when it's ready to go.", + "Your_request_to_join__roomName__has_been_made_it_could_take_up_to_15_minutes_to_be_processed": "Your request to join {{roomName}} has been made, it could take up to 15 minutes to be processed. You'll be notified when it's ready to go.", "Your_question": "Your question", "Your_server_link": "Your server link", "Your_temporary_password_is_password": "Your temporary password is [password].", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 908e6d6a9f71..9e1fed728dce 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -2570,7 +2570,7 @@ "leave-c_description": "Permiso para salir de canales", "leave-p": "Salir de grupos privados", "leave-p_description": "Permiso para salir de grupos privados", - "Lets_get_you_new_one": "Vamos a darte uno nuevo", + "Lets_get_you_new_one_": "Vamos a darte uno nuevo", "List_of_Channels": "Lista de Channels", "List_of_departments_for_forward": "Lista de departamentos permitidos para reenvío (opcional)", "List_of_departments_for_forward_description": "Permitir establecer una lista restringida de departamentos que pueden recibir chats de este departamento", @@ -4905,8 +4905,10 @@ "subscription.callout.capabilitiesDisabled": "Características desactivadas", "subscription.callout.description.limitsExceeded_one": "Su espacio de trabajo ha superado el límite de <1> {{val}} . <3> Administre su suscripción para incrementar los límites.", "subscription.callout.description.limitsExceeded_other": "Su espacio de trabajo ha superado los límites <1> {{val, list}} . <3> Administre su suscripción para incrementar los límites.", + "subscription.callout.description.limitsExceeded_many": "Su espacio de trabajo ha superado los límites <1> {{val, list}} . <3> Administre su suscripción para incrementar los límites.", "subscription.callout.description.limitsReached_one": "Su espacio de trabajo ha alcanzado el límite <1> {{val}} . <3> Administre su suscripción para incrementar los límites.", "subscription.callout.description.limitsReached_other": "Su espacio de trabajo ha alcanzado los límites <1> {{val, list}} . <3> Administre su suscripción para incrementar los límites.", + "subscription.callout.description.limitsReached_many": "Su espacio de trabajo ha alcanzado los límites <1> {{val, list}} . <3> Administre su suscripción para incrementar los límites.", "subscription.callout.allPremiumCapabilitiesDisabled": "Todas las funciones premium desactivadas", "subscription.callout.activeUsers": "puestos", "subscription.callout.guestUsers": "invitados", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index 191fe4a82ea0..6cc6bae167e6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -2908,7 +2908,7 @@ "leave-c_description": "Oikeus poistua kanavilta", "leave-p": "Poistu yksityisistä ryhmistä", "leave-p_description": "Oikeus poistua yksityisistä ryhmistä", - "Lets_get_you_new_one": "Hankitaan uusi!", + "Lets_get_you_new_one_": "Hankitaan uusi!", "License": "Käyttöoikeus", "Line": "Rivi", "Link": "Linkki", @@ -4187,7 +4187,7 @@ "SAML_AuthnContext_Template": "AuthnContext-malli", "SAML_AuthnContext_Template_Description": "Voit käyttää tässä mitä tahansa muuttujaa AuthnRequest-mallista. \n \n Jos haluat lisätä lisää authn-konteksteja, kopioi {{AuthnContextClassRef}}-tunniste ja korvaa {{\\_\\_authnContext\\_\\}}-muuttuja uudella kontekstilla.", "SAML_AuthnRequest_Template": "AuthnRequest-malli", - "SAML_AuthnRequest_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_newId\\__\\_**: Satunnaisesti luotu id-merkkijono \n- **\\__\\_instant\\_\\_\\_**: Nykyinen aikaleima \n- **\\_\\_callbackUrl\\_\\_**: Rocket.Chatin takaisinkutsun URL-osoite. \n- **\\_\\_entryPoint\\_\\_**: {{Custom Entry Point}} -asetuksen arvo. \n- **\\____________**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormatTag\\_\\_**: __NameID-käytäntömallin__ sisältö, jos voimassa oleva {{Identifier Format}} on määritetty. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\_authnContextTag\\_\\_**: __AuthnContext-mallin__ sisältö, jos voimassa oleva {{Custom Authn Context}} on määritetty. \n- **\\_\\_authnContextComparison\\_\\_**: {{Authn Context Comparison}} -asetuksen arvo. \n- **\\_\\_authnContext\\_\\_**: {{Custom Authn Context}} -asetuksen arvo.", + "SAML_AuthnRequest_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_newId\\_\\_**: Satunnaisesti luotu id-merkkijono \n- **\\_\\_instant\\_\\_**: Nykyinen aikaleima \n- **\\_\\_callbackUrl\\_\\_**: Rocket.Chatin takaisinkutsun URL-osoite. \n- **\\_\\_entryPoint\\_\\_**: {{Custom Entry Point}} -asetuksen arvo. \n- **\\_\\_issuer\\_\\_**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormatTag\\_\\_**: {{NameID Policy Template}} sisältö, jos voimassa oleva {{Identifier Format}} on määritetty. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\_authnContextTag\\_\\_**: {{AuthnContext Template}} sisältö, jos voimassa oleva {{Custom Authn Context}} on määritetty. \n- **\\_\\_authnContextComparison\\_\\_**: {{Authn Context Comparison}} -asetuksen arvo. \n- **\\_\\_authnContext\\_\\_**: {{Custom Authn Context}} -asetuksen arvo.", "SAML_Connection": "Yhteys", "SAML_Enterprise": "Yritys", "SAML_General": "Yleinen", @@ -4236,7 +4236,7 @@ "SAML_LogoutResponse_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_newId\\__\\_**: Satunnaisesti luotu id-merkkijono \n- **\\_\\_inResponseToId\\_\\_**: IdP:ltä vastaanotetun uloskirjautumispyynnön tunnus \n- **\\_\\_instant\\_\\__**: Nykyinen aikaleima \n- **\\_\\_idpSLORedirectURL\\_\\_**: IDP:n yksittäisen uloskirjautumisen URL-osoite, johon ohjataan. \n- **\\_\\_issuer\\_\\__**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\__nameID\\_\\__**: IdP:n uloskirjautumispyynnöstä saatu NameID. \n- **\\_\\_sessionIndex\\_\\_**: IdP:n uloskirjautumispyynnöstä saatu sessionIndex.", "SAML_Metadata_Certificate_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_certificate\\_\\_**: Yksityinen varmenne väitteen salausta varten.", "SAML_Metadata_Template": "Metadatan tietomalli", - "SAML_Metadata_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_sloLocation\\_\\_**: Rocket.Chat Single LogOut URL-osoite. \n- **\\____issuer\\_____**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\_certificateTag\\_\\_**: Jos yksityinen varmenne on määritetty, tämä sisältää {{Metadata Certificate Template}} -varmenteen mallin__, muutoin sitä ei oteta huomioon. \n- **\\_\\_callbackUrl\\_\\_**: Rocket.Chatin takaisinkutsun URL-osoite.", + "SAML_Metadata_Template_Description": "Seuraavat muuttujat ovat käytettävissä: \n- **\\_\\_sloLocation\\_\\_**: Rocket.Chat Single LogOut URL-osoite. \n- **\\_\\_issuer\\_\\_**: {{Custom Issuer}} -asetuksen arvo. \n- **\\_\\_identifierFormat\\_\\_**: {{Identifier Format}} -asetuksen arvo. \n- **\\_\\_certificateTag\\_\\_**: Jos yksityinen varmenne on määritetty, tämä sisältää {{Metadata Certificate Template}} -varmenteen mallin__, muutoin sitä ei oteta huomioon. \n- **\\_\\_callbackUrl\\_\\_**: Rocket.Chatin takaisinkutsun URL-osoite.", "SAML_MetadataCertificate_Template": "Metadatan varmenteen malli", "SAML_NameIdPolicy_Template": "NameID Policy malli", "SAML_NameIdPolicy_Template_Description": "Voit käyttää mitä tahansa muuttujaa Authorize Request Template -mallista.", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index 816a042332a8..f239fcc0fc74 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -2569,7 +2569,7 @@ "leave-c_description": "Autorisation de quitter les canaux", "leave-p": "Quitter les groupes privés", "leave-p_description": "Autorisation de quitter les groupes privés", - "Lets_get_you_new_one": "Nous allons vous en fournir un nouveau", + "Lets_get_you_new_one_": "Nous allons vous en fournir un nouveau", "List_of_Channels": "Liste des canaux", "List_of_departments_for_forward": "Liste des départements autorisés pour le transfert (optionnel)", "List_of_departments_for_forward_description": "Autoriser à définir une liste restreinte de départements qui peuvent recevoir des chats de ce département", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index 670e353850f0..0f9e12f8b599 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -2801,7 +2801,7 @@ "leave-c_description": "Jogosultság a csatornák elhagyásához", "leave-p": "Személyes csoportok elhagyása", "leave-p_description": "Jogosultság a személyes csoportok elhagyásához", - "Lets_get_you_new_one": "Had adjunk Önnek egy újat!", + "Lets_get_you_new_one_": "Had adjunk Önnek egy újat!", "License": "Licenc", "Link_Preview": "Hivatkozás előnézete", "List_of_Channels": "Csatornák listája", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json index adda184a03b8..4a656c8b3337 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json @@ -21,7 +21,7 @@ "24_Hour": "Orologio 24 ore", "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Un nuovo proprietario verrà assegnato automaticamente a{{count}}stanze.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Un nuovo proprietario verrà assegnato automaticamente alla stanza {{roomName}}.", - "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nuovo proprietario verrà assegnato automaticamente a queste_count__stanze:
__rooms__.", + "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nuovo proprietario verrà assegnato automaticamente a queste_count__stanze:
{{rooms}}.", "Accept_Call": "Accetta la chiamata", "Accept": "Accetta", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accetta richieste livechat in arrivo anche se non c'è alcun operatore online", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 80b1dcafa5d1..9487983bf9f7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -2546,7 +2546,7 @@ "leave-c_description": "チャネルから退出する権限", "leave-p": "プライベートグループから退出", "leave-p_description": "プライベートグループから退出する権限", - "Lets_get_you_new_one": "新たな挑戦をしてみましょう!", + "Lets_get_you_new_one_": "新たな挑戦をしてみましょう!", "List_of_Channels": "Channel一覧", "List_of_departments_for_forward": "転送が許可されている部署の一覧(オプション)", "List_of_departments_for_forward_description": "この部署からチャットを受信できる部署の制限リストを設定することを許可します", @@ -3615,7 +3615,7 @@ "SAML_General": "一般", "SAML_Custom_Authn_Context": "カスタム認証コンテキスト", "SAML_Custom_Authn_Context_Comparison": "認証コンテキストの比較", - "SAML_Custom_Authn_Context_description": "要求からauthnコンテキストを除外するには、これを空のままにします。 \n \n複数の認証コンテキストを追加するには、__AuthnContextTemplate__設定に直接追加します。", + "SAML_Custom_Authn_Context_description": "要求からauthnコンテキストを除外するには、これを空のままにします。 \n \n複数の認証コンテキストを追加するには、{{AuthnContext Template}}設定に直接追加します。", "SAML_Custom_Cert": "カスタム証明書", "SAML_Custom_Debug": "デバッグを有効にする", "SAML_Custom_EMail_Field": "メールのフィールド名", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index 7f3d31510b6f..8e673e5346cc 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -2059,7 +2059,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "დატოვეთ აღწერილობის ველი ცარიელი, თუ არ გსურთ როლის ჩვენება", "leave-c": "დატოვეთ არხები", "leave-p": "დატოვე პირადი ჯგუფები", - "Lets_get_you_new_one": "მიიღეთ ახალი!", + "Lets_get_you_new_one_": "მიიღეთ ახალი!", "List_of_Channels": "არხების სია", "List_of_departments_for_forward": "გასაგზავნად ნებადართული განყოფილებების სია (არჩევითი)", "List_of_departments_for_forward_description": "ნება დართეთ შეიქმნას შეზღუდული სია (განყოფილებების) , რომელთაც შეუძლიათ მიიღონ ჩათები ამ განყოფილებიდან", @@ -2803,8 +2803,8 @@ "Room_archivation_state_false": "აქტიური", "Room_archivation_state_true": "დაარქივებულია", "Room_archived": "ოთახი დაარქივებულია", - "room_changed_announcement": "ოთახის განცხადება შეიცვალა __room_announcement__,__username__-ის მიერ", - "room_changed_description": "ოთახის აღწერა შეიცვალა: __room_description__ __ მომხმარებელი__-ის მიერ ", + "room_changed_announcement": "ოთახის განცხადება შეიცვალა {{room_announcement}},{{username}}-ის მიერ", + "room_changed_description": "ოთახის აღწერა შეიცვალა: {{room_description}} __ მომხმარებელი__-ის მიერ ", "room_changed_topic": "ოთახის თემა შეიცვალა: {{room_topic}} {{user_by}}", "Room_default_change_to_private_will_be_default_no_more": "ეს არის დეფაულტ არხი და პირად ჯგუფად გადაკეთების შემთხვევაში აღარ იქნება დეფაულტ არხი.გსურთ გაგრძელება?", "Room_description_changed_successfully": "ოთახის აღწერა წარმატებით შეიცვალა", @@ -3247,7 +3247,7 @@ "This_room_has_been_archived_by__username_": "ეს ოთახი დაარქივდა {{username}}-ის მიერ", "This_room_has_been_unarchived_by__username_": "ეს ოთახი ამოარქივდა {{username}}-ის მიერ", "This_week": "ეს კვირა", - "Thread_message": "კომენტარი გააკეთა * __ მომხმარებლის __ ის გზავნილზე: _ __msg__ _", + "Thread_message": "კომენტარი გააკეთა * __ მომხმარებლის __ ის გზავნილზე: _ {{msg}} _", "Thursday": "ხუთშაბათი", "Time_in_seconds": "დრო წამებში", "Timeouts": "თაიმაუტები", @@ -3432,7 +3432,7 @@ "User_removed_by": "მომხმარებელი {{user_removed}} {{user_by}}.", "User_sent_a_message_on_channel": "{{username}} შეტყობინების გაგზავნა {{channel}}", "User_sent_a_message_to_you": "{{username}} გამოგიგზავნათ შეტყობინება", - "user_sent_an_attachment": "__username__– მა გაგზავნა დანართი", + "user_sent_an_attachment": "{{username}}– მა გაგზავნა დანართი", "User_Settings": "მომხმარებლის პარამეტრები", "User_started_a_new_conversation": "{{username}}– მა დაიწყო ახალი საუბარი", "User_unmuted_by": "მომხმარებელი {{user_unmuted}} {{user_by}}.", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json index 8acc4f3c84d4..b58c8c5c5dc1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json @@ -1815,7 +1815,7 @@ "Leave_the_current_channel": "ចាកចេញពីឆានែលបច្ចុប្បន្ន", "leave-c": "ចាកចេញពីឆានែល", "leave-p": "ចាកចេញពីក្រុមឯកជន", - "Lets_get_you_new_one": "ចូរយើងទទួលបានអ្នកថ្មី!", + "Lets_get_you_new_one_": "ចូរយើងទទួលបានអ្នកថ្មី!", "List_of_Channels": "បញ្ជីឆានែល", "List_of_Direct_Messages": "បញ្ជីនៃការផ្ញើសារដោយផ្ទាល់", "Livechat": "Livechat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 94c8816e57b2..0341f6388348 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -2227,7 +2227,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "역할을 표시하지 않으려면 설명 필드를 비워두세요.", "leave-c": "Channel 나가기", "leave-p": "비공개 그룹에서 나가기", - "Lets_get_you_new_one": "새로 생성", + "Lets_get_you_new_one_": "새로 생성", "List_of_Channels": "Channel 목록", "List_of_departments_for_forward": "전달이 허용 된 부서 목록 (선택 사항)", "List_of_departments_for_forward_description": "이 부서에서 채팅을 받을 수 있는 제한된 부서 목록을 설정하도록 허용", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json index ebf318f3aa07..19da31a1b2e5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json @@ -2555,15 +2555,15 @@ "User_Presence": "Хэрэглэгчийн байдал", "User_removed": "Хэрэглэгч устгагдсан", "User_removed_by": "Хэрэглэгч {{user_removed}}хасагдсан {{user_by}}.", - "User_sent_a_message_on_channel": "__усername__мессеж илгээгдсэн __channel__", + "User_sent_a_message_on_channel": "{{username}} мессеж илгээгдсэн {{channel}}", "User_sent_a_message_to_you": "__зориулагчтанд зурвас илгээж байна", "user_sent_an_attachment": "{{user}} хавсралтыг илгээсэн", "User_Settings": "Хэрэглэгчийн тохиргоо", "User_unmuted_by": "Хэрэглэгч {{user_unmuted}}нээгдсэн {{user_by}}.", "User_unmuted_in_room": "Хэрэглэгчид унтаагүй байна", "User_updated_successfully": "Хэрэглэгч шинэчлэгдсэн", - "User_uploaded_a_file_on_channel": "__усername____channel__ дээр файл байршуулсан", - "User_uploaded_a_file_to_you": "__усername__танд файл илгээв", + "User_uploaded_a_file_on_channel": "{{username}} {{channel}} дээр файл байршуулсан", + "User_uploaded_a_file_to_you": "{{username}} танд файл илгээв", "User_uploaded_file": "Файлыг байршуулсан", "User_uploaded_image": "Зургийг байршуулсан", "user-generate-access-token": "Хэрэглэгч нэвтрэх тэмдгийг үүсгэнэ", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json index dcc6057f2efc..8d2cb17ea0d2 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json @@ -1,6 +1,6 @@ { "500": "Ralat Pelayan Dalaman", - "__username__was_set__role__by__user_by_": "__nama pengguna__ ditubuhkan __role__ oleh __user_by__", + "__username__was_set__role__by__user_by_": "__nama pengguna__ ditubuhkan {{role}} oleh {{user_by}}", "@username": "@pengguna", "@username_message": "@username ", "#channel": "#channel", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index 25b0a24ca538..23e356f28eee 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -2563,7 +2563,7 @@ "leave-c_description": "Toestemming om kanalen te verlaten", "leave-p": "Verlaat privégroepen", "leave-p_description": "Toestemming om privégroepen te verlaten", - "Lets_get_you_new_one": "Laten we een nieuwe voor je regelen!", + "Lets_get_you_new_one_": "Laten we een nieuwe voor je regelen!", "List_of_Channels": "Lijst met kanalen", "List_of_departments_for_forward": "Lijst met afdelingen die mogen worden doorgestuurd (optioneel)", "List_of_departments_for_forward_description": "Sta toe om een beperkte lijst van afdelingen in te stellen die chats van deze afdeling kunnen ontvangen", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 3c315fcd5b99..aa8ca813b445 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -2761,7 +2761,7 @@ "leave-c_description": "Zezwolenie na opuszczenie kanałów", "leave-p": "Opuść grupy prywatne", "leave-p_description": "Zezwolenie na opuszczenie grup prywatnych", - "Lets_get_you_new_one": "Zróbmy ci nową!", + "Lets_get_you_new_one_": "Zróbmy ci nową!", "License": "Licencja", "Link_Preview": "Podgląd linków", "List_of_Channels": "Lista kanałów", @@ -5163,9 +5163,9 @@ "You": "ty", "You_reacted_with": "Zareagowałeś z {{emoji}}", "Users_reacted_with": "{{users}} zareagowali z {{emoji}}", - "Users_and_more_reacted_with": "__users__ i __count__ więcej zareagowali z __emoji__", + "Users_and_more_reacted_with": "{{users}} i {{count}} więcej zareagowali z {{emoji}}", "You_and_users_Reacted_with": "Ty i {{users}} zareagowali z {{emoji}}", - "You_users_and_more_Reacted_with": "Ty, __users__ i __count__ więcej zareagowali z __emoji__", + "You_users_and_more_Reacted_with": "Ty, {{users}} i {{count}} więcej zareagowali z {{emoji}}", "You_are_converting_team_to_channel": "Przekształcasz ten zespół w kanał.", "you_are_in_preview_mode_of": "Jesteś w trybie podglądu kanału # {{room_name}}", "you_are_in_preview_mode_of_incoming_livechat": "Jesteś w trybie podglądu wiadomości przychodzącej livechat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 90ed0d6db384..4daea7308d8a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -2618,7 +2618,7 @@ "leave-c_description": "Permissão para deixar canais", "leave-p": "Deixar grupos privados", "leave-p_description": "Permissão para deixar grupos privados", - "Lets_get_you_new_one": "Vamos pegar outro!", + "Lets_get_you_new_one_": "Vamos pegar outro!", "List_of_Channels": "Lista de Canais", "List_of_departments_for_forward": "Lista de departamentos permitidos para encaminhamento (opcional).", "List_of_departments_for_forward_description": "Permite definir uma lista restrita de departamentos que podem receber conversas deste departamento.", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json index 367c9c2cd97b..9bc920849c8c 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -1803,7 +1803,7 @@ "Leave_the_current_channel": "Sai deste canal", "leave-c": "Sair dos canais", "leave-p": "Sair dos grupos privados", - "Lets_get_you_new_one": "Vamos pegar uma nova!", + "Lets_get_you_new_one_": "Vamos pegar uma nova!", "List_of_Channels": "Lista de Canais", "List_of_Direct_Messages": "Lista de Mensagens Diretas", "Livechat": "Livechat", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index e1699812a3d9..e3dc8d0a5791 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -2710,7 +2710,7 @@ "leave-c_description": "Разрешение покидать каналы", "leave-p": "Оставить личные группы", "leave-p_description": "Разрешение покидать приватные группы", - "Lets_get_you_new_one": "Давайте получим новый!", + "Lets_get_you_new_one_": "Давайте получим новый!", "License": "Лицензия", "List_of_Channels": "Список чатов", "List_of_departments_for_forward": "Список департаментов, разрешенных к перенаправлению (необязательно)", @@ -3841,8 +3841,8 @@ "SAML_LogoutRequest_Template": "Шаблон запроса на выход из системы", "SAML_LogoutRequest_Template_Description": "Доступны следующие переменные: \n- **\\_\\_\\_newId\\_\\_**: Случайно сгенерированная строка идентификатора \n- **\\_\\_\\_\\_стоянная\\_\\_**: Текущая метка времени \n- **\\_\\_idpSLORedirectURL\\_\\_**: URL IDP Single LogOut для перенаправления. \n- **\\_\\_\\_\\_issuer\\_\\_**: Значение параметра {{Custom Issuer}}. \n- **\\_\\_identifierFormat\\_\\_**: Значение параметра __Формат идентификатора__. \n- **\\_\\_\\_\\_nameID\\_\\_\\_**: Идентификатор имени, полученный от IdP, когда пользователь вошел в систему. \n- **\\_\\_sessionIndex\\_\\_**: Индекс сессии, полученный от IdP, когда пользователь вошел в систему.", "SAML_LogoutResponse_Template": "Шаблон выхода из системы", - "SAML_LogoutResponse_Template_Description": "Доступны следующие переменные: \n- **__newId__**: Случайно сгенерированная идентификационная строка \n- **__inResponseToId__**: Идентификатор запроса на выход из системы, полученный от IdP \n- **instant_**: Текущая метка времени \n- **__idpSLORedirectURL__**: URL одиночного входа в систему IDP для переадресации. \n- **issuer_**: Значение параметра {{Custom Issuer}}. \n- **{{identifierFormat}}**: Значение параметра {{Identifier Format}}. \n- **__nameID___**: Идентификатор имени, полученный из запроса на выход из системы IdP. \n- **__sessionIndex__**: СессияИндекс, полученный из запроса на выход из системы IdP.", - "SAML_Metadata_Certificate_Template_Description": "Доступны следующие переменные: \n- **__certificate__**: Частный сертификат для шифрования утверждения.", + "SAML_LogoutResponse_Template_Description": "Доступны следующие переменные: \n- **\\_\\_newId\\_\\_**: Случайно сгенерированная идентификационная строка \n- **\\_\\_inResponseToId\\_\\_**: Идентификатор запроса на выход из системы, полученный от IdP \n- **\\_\\_instant\\_\\_**: Текущая метка времени \n- **\\_\\_idpSLORedirectURL\\_\\_**: URL одиночного входа в систему IDP для переадресации. \n- **\\_\\_issuer\\_\\_**: Значение параметра {{Custom Issuer}}. \n- **\\_\\_identifierFormat\\_\\_**: Значение параметра {{Identifier Format}}. \n- **\\_\\_nameID\\_\\_**: Идентификатор имени, полученный из запроса на выход из системы IdP. \n- **\\_\\_sessionIndex\\_\\_**: СессияИндекс, полученный из запроса на выход из системы IdP.", + "SAML_Metadata_Certificate_Template_Description": "Доступны следующие переменные: \n- **\\_\\_certificate\\_\\_**: Частный сертификат для шифрования утверждения.", "SAML_Metadata_Template": "Шаблон метаданных", "SAML_Metadata_Template_Description": "Доступны следующие переменные: \n- **\\_\\_sloLocation\\_\\_**:URL одиночного входа в систему Rocket.Chat. \n- **\\__\\issuer\\__\\_**: Значение параметра {{Custom Issuer}}. \n- **\\_\\_identifierFormat\\_\\_**: Значение параметра {{Identifier Format}}. \n- **\\__\\certificateTag\\__\\_**: Если настроен личный сертификат, он будет включать {{Metadata Certificate Template}}, в противном случае он будет проигнорирован. \n- **\\__\\callbackUrl\\__\\_**: URL обратного вызова Rocket.Chat.", "SAML_MetadataCertificate_Template": "Шаблон сертификата метаданных", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index 01ef2e16e50c..94964cee15d6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -2909,7 +2909,7 @@ "leave-c_description": "Behörighet att lämna kanaler", "leave-p": "Lämna privata grupper", "leave-p_description": "Tillstånd att lämna privata grupper", - "Lets_get_you_new_one": "Vi ordnar en ny.", + "Lets_get_you_new_one_": "Vi ordnar en ny.", "License": "Licens", "Line": "Linje", "Link": "Länk", @@ -5732,7 +5732,7 @@ "RegisterWorkspace_Token_Step_Two": "Kopiera ditt token och klistra in det nedan.", "RegisterWorkspace_with_email": "Registrera arbetsytan med e-post", "RegisterWorkspace_Setup_Subtitle": "För att registrera arbetsytan måste det associeras med ett Rocket.Chat Cloud-konto.", - "RegisterWorkspace_Setup_Steps": "Steg __steg__ av __numberOfSteps__", + "RegisterWorkspace_Setup_Steps": "Steg {{step}} av {{numberOfSteps}}", "RegisterWorkspace_Setup_Label": "E-postadress för molnkonto", "RegisterWorkspace_Setup_Have_Account_Title": "Har du ett konto?", "RegisterWorkspace_Setup_Have_Account_Subtitle": "Ange din e-postadress till Cloud-kontot för att koppla arbetsytan till ditt konto.", @@ -5747,7 +5747,7 @@ "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Jag godkänner <1>villkoren och <3>integritetspolicyn", "Larger_amounts_of_active_connections": "För större mängder aktiva anslutningar kan du överväga vår", "Uninstall_grandfathered_app": "Avinstallera {{appName}}?", - "App_will_lose_grandfathered_status": "**Denna {{context}}-app kommer att förlora sin status som gammal app.** \n \nArbetsytorna i Community Edition kan ha upp till {{limit}} __kontext__-appar aktiverade. Gamla appar inkluderas i gränsen, men gränsen tillämpas inte på dem.", + "App_will_lose_grandfathered_status": "**Denna {{context}}-app kommer att förlora sin status som gammal app.** \n \nArbetsytorna i Community Edition kan ha upp till {{limit}} {{context}}-appar aktiverade. Gamla appar inkluderas i gränsen, men gränsen tillämpas inte på dem.", "Theme_Appearence": "Utseende för tema", "Enterprise": "Enterprise", "UpgradeToGetMore_engagement-dashboard_Title": "Analytics" diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json index 33ed4e6d83df..89366d9df2a0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json @@ -2524,7 +2524,7 @@ "Use_url_for_avatar": "சின்னம் URL ஐ பயன்படுத்த", "Use_User_Preferences_or_Global_Settings": "பயனர் விருப்பங்கள் அல்லது உலகளாவிய அமைப்புகள் பயன்படுத்தவும்", "User": "பயனர்", - "User__username__is_now_a_leader_of__room_name_": "பயனர் __இயக்குநர்__ இப்போது __room_name__ இன் தலைவர்", + "User__username__is_now_a_leader_of__room_name_": "பயனர் {{username}} இப்போது {{room_name}} இன் தலைவர்", "User__username__is_now_a_moderator_of__room_name_": "பயனர் {{username}} இப்போது {{room_name}} ஒரு மதிப்பீட்டாளர்", "User__username__is_now_an_owner_of__room_name_": "பயனர் {{username}} இப்போது {{room_name}} ஒரு உரிமையாளர் ஆவார்", "User__username__removed_from__room_name__leaders": "{{room_name}} தலைவர்களிடமிருந்து பயனர் {{username}} நீக்கப்பட்டது", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json index 3485f01cd190..9f969914d5d0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -1833,7 +1833,7 @@ "Leave_the_current_channel": "Geçerli kanalı bırak", "leave-c": "Kanallardan Çık", "leave-p": "Özel Grupları Bırak", - "Lets_get_you_new_one": "Size yeni bir tane verelim!", + "Lets_get_you_new_one_": "Size yeni bir tane verelim!", "List_of_Channels": "Kanal Listesi", "List_of_Direct_Messages": "Doğrudan İletiler Listesi", "Livechat": "Canlı Görüşme", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index c1a4796794db..8bbb614c560b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -2517,7 +2517,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "如果不想顯示角色,請將描述欄位保持空白", "leave-c": "保留 Channel", "leave-p": "離開私人群組", - "Lets_get_you_new_one": "來取得新的!", + "Lets_get_you_new_one_": "來取得新的!", "List_of_Channels": "Channel 列表", "List_of_departments_for_forward": "允許轉送的部門列表(可選)", "List_of_departments_for_forward_description": "允許設定可以接收從此部門聊天記錄部門的受限列表", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index 1a698984199d..7d4a50c8e362 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -2265,7 +2265,7 @@ "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "如果不想显示对应角色,请将描述字段留空", "leave-c": "保留频道", "leave-p": "离开私人组", - "Lets_get_you_new_one": "新版本即将到来", + "Lets_get_you_new_one_": "新版本即将到来", "List_of_Channels": "频道列表", "List_of_departments_for_forward": "允许转发的部门列表(可选)", "List_of_departments_for_forward_description": "允许设置一个列表来限制可从此部门接收聊天的部门", diff --git a/apps/meteor/server/lib/i18n.ts b/apps/meteor/server/lib/i18n.ts index 265305ef71d6..bc3ed6184937 100644 --- a/apps/meteor/server/lib/i18n.ts +++ b/apps/meteor/server/lib/i18n.ts @@ -2,13 +2,20 @@ import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import i18nDict from '@rocket.chat/i18n'; import type { TOptions } from 'i18next'; -import { i18n } from '../../app/utils/lib/i18n'; +import { availableTranslationNamespaces, defaultTranslationNamespace, extractTranslationNamespaces, i18n } from '../../app/utils/lib/i18n'; void i18n.init({ lng: 'en', - defaultNS: 'core', - resources: Object.fromEntries(Object.entries(i18nDict).map(([key, value]) => [key, { core: value }])), - initImmediate: true, + defaultNS: defaultTranslationNamespace, + ns: availableTranslationNamespaces, + nsSeparator: '.', + resources: Object.fromEntries( + Object.entries(i18nDict).map(([language, source]) => [ + language, + extractTranslationNamespaces(source as unknown as Record), + ]), + ), + initImmediate: false, }); declare module 'i18next' { diff --git a/apps/meteor/server/settings/email.ts b/apps/meteor/server/settings/email.ts index 9de6ecf7efc0..fdfd43c7557a 100644 --- a/apps/meteor/server/settings/email.ts +++ b/apps/meteor/server/settings/email.ts @@ -499,7 +499,7 @@ export const createEmailSettings = () => await this.add( 'Forgot_Password_Email', - '

{Forgot_password}

{Lets_get_you_new_one}

{Reset}

{If_you_didnt_ask_for_reset_ignore_this_email}

', + '

{Forgot_password}

{Lets_get_you_new_one_}

{Reset}

{If_you_didnt_ask_for_reset_ignore_this_email}

', { type: 'code', code: 'text/html', diff --git a/packages/i18n/src/index.mjs b/packages/i18n/src/index.mjs index 41f5eb83a4d6..3204c6c3fd80 100644 --- a/packages/i18n/src/index.mjs +++ b/packages/i18n/src/index.mjs @@ -88,9 +88,13 @@ const tds = `export interface RocketchatI18n { ${keys.map((key) => `${JSON.stringify(key)}: string;`).join('\n\t')} } -export declare const dict: Record; +const dict: { + [language: string]: RocketchatI18n; +}; export type RocketchatI18nKeys = keyof RocketchatI18n; + +export = dict; `; const languages = files.map((file) => path.basename(file, '.i18n.json')); diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 37d099dcbce6..b97863140c51 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -6,6 +6,7 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/fuselage-hooks": "~0.32.1", + "@rocket.chat/i18n": "workspace:~", "@rocket.chat/rest-typings": "workspace:^", "@types/jest": "~29.5.7", "@types/react": "~17.0.69", @@ -25,6 +26,7 @@ "@rocket.chat/ddp-client": "workspace:^", "@rocket.chat/emitter": "*", "@rocket.chat/fuselage-hooks": "*", + "@rocket.chat/i18n": "workspace:~", "@rocket.chat/rest-typings": "workspace:^", "react": "~17.0.2", "use-sync-external-store": "^1.2.0" diff --git a/packages/ui-contexts/src/TranslationContext.ts b/packages/ui-contexts/src/TranslationContext.ts index b73428932d8f..d8fa4423cc27 100644 --- a/packages/ui-contexts/src/TranslationContext.ts +++ b/packages/ui-contexts/src/TranslationContext.ts @@ -1,9 +1,6 @@ +import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; import { createContext } from 'react'; -import type keys from './en.json'; - -export { keys }; - export type TranslationLanguage = { en: string; name: string; @@ -11,7 +8,7 @@ export type TranslationLanguage = { key: string; }; -export type TranslationKey = keyof typeof keys | `app-${string}.${string}`; +export type TranslationKey = RocketchatI18nKeys | `app-${string}.${string}`; export type TranslationContextValue = { languages: TranslationLanguage[]; diff --git a/packages/ui-contexts/src/en.json b/packages/ui-contexts/src/en.json deleted file mode 120000 index 2d8842c5138a..000000000000 --- a/packages/ui-contexts/src/en.json +++ /dev/null @@ -1 +0,0 @@ -../../../apps/meteor/private/i18n/en.i18n.json \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b3d14ad61044..17350a085065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9586,6 +9586,7 @@ __metadata: "@types/strict-uri-encode": ^2.0.1 "@types/string-strip-html": ^5.0.1 "@types/supertest": ^2.0.15 + "@types/supports-color": ~7.2.0 "@types/textarea-caret": ^3.0.2 "@types/ua-parser-js": ^0.7.38 "@types/use-subscription": ^1.0.1 @@ -9770,6 +9771,7 @@ __metadata: stylelint: ^14.9.1 stylelint-order: ^5.0.0 supertest: ^6.2.3 + supports-color: ~7.2.0 suretype: ~2.4.1 swiper: ^9.3.2 tar-stream: ^1.6.2 @@ -10387,6 +10389,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": ~0.31.25 "@rocket.chat/fuselage-hooks": ~0.32.1 + "@rocket.chat/i18n": "workspace:~" "@rocket.chat/password-policies": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@types/jest": ~29.5.7 @@ -10406,6 +10409,7 @@ __metadata: "@rocket.chat/ddp-client": "workspace:^" "@rocket.chat/emitter": "*" "@rocket.chat/fuselage-hooks": "*" + "@rocket.chat/i18n": "workspace:~" "@rocket.chat/rest-typings": "workspace:^" react: ~17.0.2 use-sync-external-store: ^1.2.0 @@ -14192,6 +14196,13 @@ __metadata: languageName: node linkType: hard +"@types/supports-color@npm:~7.2.0": + version: 7.2.1 + resolution: "@types/supports-color@npm:7.2.1" + checksum: abf7d9348deadf5386cf5faec062a4132e647a179584f52cace87435248f520be73c58ac28618cf5684e6b0ed6bb635d5a975cc71ff613af7db2d5648557ef45 + languageName: node + linkType: hard + "@types/tapable@npm:^1, @types/tapable@npm:^1.0.5": version: 1.0.8 resolution: "@types/tapable@npm:1.0.8" @@ -38182,7 +38193,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0, supports-color@npm:~7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: