refactor(i18n): Translation's lint and load (#31343)

pull/31374/head^2
Tasso Evangelista 2 years ago committed by GitHub
parent c5693fb8c8
commit eecf782737
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 109
      apps/meteor/.scripts/check-i18n.js
  2. 249
      apps/meteor/.scripts/translation-check.ts
  3. 21
      apps/meteor/.scripts/translation-diff.ts
  4. 6
      apps/meteor/.scripts/translation-fix-order.ts
  5. 121
      apps/meteor/app/utils/lib/i18n.ts
  6. 28
      apps/meteor/client/lib/utils/applyCustomTranslations.ts
  7. 155
      apps/meteor/client/providers/TranslationProvider.tsx
  8. 8
      apps/meteor/package.json
  9. 2
      apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json
  10. 2
      apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json
  11. 2
      apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json
  12. 2
      apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json
  13. 2
      apps/meteor/packages/rocketchat-i18n/i18n/de-IN.i18n.json
  14. 2
      apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json
  15. 4
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
  16. 4
      apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json
  17. 6
      apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json
  18. 2
      apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json
  19. 2
      apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json
  20. 2
      apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json
  21. 4
      apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json
  22. 10
      apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json
  23. 2
      apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json
  24. 2
      apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json
  25. 6
      apps/meteor/packages/rocketchat-i18n/i18n/mn.i18n.json
  26. 2
      apps/meteor/packages/rocketchat-i18n/i18n/ms-MY.i18n.json
  27. 2
      apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json
  28. 6
      apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json
  29. 2
      apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  30. 2
      apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json
  31. 6
      apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json
  32. 6
      apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json
  33. 2
      apps/meteor/packages/rocketchat-i18n/i18n/ta-IN.i18n.json
  34. 2
      apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json
  35. 2
      apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json
  36. 2
      apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json
  37. 15
      apps/meteor/server/lib/i18n.ts
  38. 2
      apps/meteor/server/settings/email.ts
  39. 6
      packages/i18n/src/index.mjs
  40. 2
      packages/ui-contexts/package.json
  41. 7
      packages/ui-contexts/src/TranslationContext.ts
  42. 1
      packages/ui-contexts/src/en.json
  43. 13
      yarn.lock

@ -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);
}
})();

@ -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<string, string>) => {
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<string, string>;
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<string, string>, 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<string, string>) => {
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<string, string>; 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<string, string>, 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<string, string>;
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<string, string>;
path: PathLike;
lng: string;
sourceJson: Record<string, string>;
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);
});

@ -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<string, string>, b: Record<string, string>) {
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));
});

@ -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);

@ -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<string, string>): Record<TranslationNamespace, Record<string, string>> => {
const result: Record<TranslationNamespace, Record<string, string>> = {
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<string, string>, 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<RocketchatI18nKeys, `${string}.${string}`> extends `${infer T}.${string}` ? (T extends Lowercase<T> ? T : never) : never)
| 'core';
const namespacesMap: Record<TranslationNamespace, true> = {
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<string, Record<string, string>>,
{ 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);
}
}
};

@ -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<string, unknown> = 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);
}
};

@ -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<TranslationKey, `${string}.${string}`> extends `${infer T}.${string}`
? T extends Lowercase<T>
? T
: never
: never;
const namespacesDefault = ['core', 'onboarding', 'registration', 'cloud'] as TranslationNamespace[];
const parseToJSON = (customTranslations: string): Record<string, Record<string, string>> | false => {
try {
return JSON.parse(customTranslations);
} catch (e) {
return false;
}
};
const localeCache = new Map<string, Promise<string>>();
const useI18next = (lng: string): typeof i18next => {
const useCustomTranslations = (i18n: typeof i18next) => {
const customTranslations = useSetting('Custom_Translations');
const parsedCustomTranslations = useMemo(() => {
const parsedCustomTranslations = useMemo((): Record<string, Record<string, string>> | 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<string, string>, 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<string, Promise<string>>();
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<string, Record<string, string>> => {
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<string, Record<string, string>>);
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<string>('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<void> => {
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'],

@ -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"

@ -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": "قائمة الأقسام المسموح بإعادة توجيهها (اختياري)",

@ -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",

@ -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",

@ -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",

@ -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",

@ -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",

@ -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 <strong>[password]</strong>.",

@ -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}} </1>. <3> Administre su suscripción</3> para incrementar los límites.",
"subscription.callout.description.limitsExceeded_other": "Su espacio de trabajo ha superado los límites <1> {{val, list}} </1>. <3> Administre su suscripción </3> para incrementar los límites.",
"subscription.callout.description.limitsExceeded_many": "Su espacio de trabajo ha superado los límites <1> {{val, list}} </1>. <3> Administre su suscripción </3> para incrementar los límites.",
"subscription.callout.description.limitsReached_one": "Su espacio de trabajo ha alcanzado el límite <1> {{val}} </1>. <3> Administre su suscripción </3> para incrementar los límites.",
"subscription.callout.description.limitsReached_other": "Su espacio de trabajo ha alcanzado los límites <1> {{val, list}} </1>. <3> Administre su suscripción </3> para incrementar los límites.",
"subscription.callout.description.limitsReached_many": "Su espacio de trabajo ha alcanzado los límites <1> {{val, list}} </1>. <3> Administre su suscripción </3> para incrementar los límites.",
"subscription.callout.allPremiumCapabilitiesDisabled": "Todas las funciones premium desactivadas",
"subscription.callout.activeUsers": "puestos",
"subscription.callout.guestUsers": "invitados",

@ -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.",

@ -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",

@ -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",

@ -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<span style=\"font-weight: bold;\">{{count}}</span>stanze.",
"A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Un nuovo proprietario verrà assegnato automaticamente alla stanza <span style=\"font-weight: bold;\">{{roomName}}</span>.",
"A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nuovo proprietario verrà assegnato automaticamente a queste<span style=\"font-weight: bold;\">_count__</span>stanze:<br/> __rooms__.",
"A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nuovo proprietario verrà assegnato automaticamente a queste<span style=\"font-weight: bold;\">_count__</span>stanze:<br/> {{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",

@ -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": "メールのフィールド名",

@ -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": "ოთახის განცხადება შეიცვალა <em>__room_announcement__</em>,__username__-ის მიერ",
"room_changed_description": "ოთახის აღწერა შეიცვალა: <em>__room_description__</em> <em> __ მომხმარებელი__-ის მიერ </em>",
"room_changed_announcement": "ოთახის განცხადება შეიცვალა <em>{{room_announcement}}</em>,{{username}}-ის მიერ",
"room_changed_description": "ოთახის აღწერა შეიცვალა: <em>{{room_description}}</em> <em> __ მომხმარებელი__-ის მიერ </em>",
"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": "<strong>{{username}}</strong> შეტყობინების გაგზავნა <strong>{{channel}}</strong>",
"User_sent_a_message_to_you": "<strong>{{username}}</strong> გამოგიგზავნათ შეტყობინება",
"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}}.",

@ -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",

@ -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": "이 부서에서 채팅을 받을 수 있는 제한된 부서 목록을 설정하도록 허용",

@ -2555,15 +2555,15 @@
"User_Presence": "Хэрэглэгчийн байдал",
"User_removed": "Хэрэглэгч устгагдсан",
"User_removed_by": "Хэрэглэгч {{user_removed}}хасагдсан {{user_by}}.",
"User_sent_a_message_on_channel": "<strong>__усername__</strong>мессеж илгээгдсэн <strong>__channel__</strong>",
"User_sent_a_message_on_channel": "<strong>{{username}}</strong> мессеж илгээгдсэн <strong>{{channel}}</strong>",
"User_sent_a_message_to_you": "<strong>__зориулагч</strong>танд зурвас илгээж байна",
"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": "<strong>__усername__</strong><strong>__channel__</strong> дээр файл байршуулсан",
"User_uploaded_a_file_to_you": "<strong>__усername__</strong>танд файл илгээв",
"User_uploaded_a_file_on_channel": "<strong>{{username}}</strong> <strong>{{channel}}</strong> дээр файл байршуулсан",
"User_uploaded_a_file_to_you": "<strong>{{username}}</strong> танд файл илгээв",
"User_uploaded_file": "Файлыг байршуулсан",
"User_uploaded_image": "Зургийг байршуулсан",
"user-generate-access-token": "Хэрэглэгч нэвтрэх тэмдгийг үүсгэнэ",

@ -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 <message>",
"#channel": "#channel",

@ -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",

@ -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 # <strong>{{room_name}}</strong>",
"you_are_in_preview_mode_of_incoming_livechat": "Jesteś w trybie podglądu wiadomości przychodzącej livechat",

@ -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.",

@ -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",

@ -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": "Шаблон сертификата метаданных",

@ -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</1> och <3>integritetspolicyn</3>",
"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"

@ -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}} நகபபடடத",

@ -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",

@ -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": "允許設定可以接收從此部門聊天記錄部門的受限列表",

@ -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": "允许设置一个列表来限制可从此部门接收聊天的部门",

@ -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<string, string>),
]),
),
initImmediate: false,
});
declare module 'i18next' {

@ -499,7 +499,7 @@ export const createEmailSettings = () =>
await this.add(
'Forgot_Password_Email',
'<h2>{Forgot_password}</h2><p>{Lets_get_you_new_one}</p><a class="btn" href="[Forgot_Password_Url]">{Reset}</a><p class="advice">{If_you_didnt_ask_for_reset_ignore_this_email}</p>',
'<h2>{Forgot_password}</h2><p>{Lets_get_you_new_one_}</p><a class="btn" href="[Forgot_Password_Url]">{Reset}</a><p class="advice">{If_you_didnt_ask_for_reset_ignore_this_email}</p>',
{
type: 'code',
code: 'text/html',

@ -88,9 +88,13 @@ const tds = `export interface RocketchatI18n {
${keys.map((key) => `${JSON.stringify(key)}: string;`).join('\n\t')}
}
export declare const dict: Record<string, RocketchatI18nKeys>;
const dict: {
[language: string]: RocketchatI18n;
};
export type RocketchatI18nKeys = keyof RocketchatI18n;
export = dict;
`;
const languages = files.map((file) => path.basename(file, '.i18n.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"

@ -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[];

@ -1 +0,0 @@
../../../apps/meteor/private/i18n/en.i18n.json

@ -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:

Loading…
Cancel
Save