Skip to content

Commit

Permalink
Add check for obsolete placeholder format
Browse files Browse the repository at this point in the history
  • Loading branch information
tassoevan committed Dec 29, 2023
1 parent fdba26e commit 82aa840
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 58 deletions.
138 changes: 111 additions & 27 deletions apps/meteor/.scripts/translation-check.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import fs from 'fs';
import path from 'path';
import type { PathLike } from 'node:fs';
import fs 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';

const regexVar = /__[a-zA-Z_]+__/g;
import i18next from 'i18next';

const validateKeys = (json: Record<string, string>, usedKeys: { key: string; replaces: string[] }[]) =>
usedKeys
Expand All @@ -20,27 +22,31 @@ const validateKeys = (json: Record<string, string>, usedKeys: { key: string; rep
return prev;
}, [] as { key: string; miss: string[] }[]);

const removeMissingKeys = (i18nFiles: string[], usedKeys: { key: string; replaces: string[] }[]) => {
i18nFiles.forEach((file) => {
const json = JSON.parse(fs.readFileSync(file, 'utf8'));
const removeMissingKeys = async (i18nFiles: string[], usedKeys: { key: string; replaces: string[] }[]) => {
for await (const file of i18nFiles) {
const json = await parseFile(file);
if (Object.keys(json).length === 0) {
return;
}

validateKeys(json, usedKeys).forEach(({ key }) => {
json[key] = null;
delete json[key];
});

fs.writeFileSync(file, JSON.stringify(json, null, 2));
});
await writeFile(file, JSON.stringify(json, null, 2), 'utf8');
}
};

const checkUniqueKeys = (content: string, json: Record<string, string>, filename: string) => {
const hasDuplicatedKeys = (content: string, json: Record<string, string>) => {
const matchKeys = content.matchAll(/^\s+"([^"]+)"/gm);

const allKeys = [...matchKeys];

if (allKeys.length !== Object.keys(json).length) {
return allKeys.length !== Object.keys(json).length;
};

const checkUniqueKeys = (content: string, json: Record<string, string>, filename: string) => {
if (hasDuplicatedKeys(content, json)) {
throw new Error(`Duplicated keys found on file ${filename}`);
}
};
Expand Down Expand Up @@ -72,13 +78,96 @@ const validate = (i18nFiles: string[], usedKeys: { key: string; replaces: string
}
};

const checkFiles = async (sourcePath: string, sourceFile: string, fix = false) => {
const content = fs.readFileSync(path.join(sourcePath, sourceFile), 'utf8');
const sourceContent = JSON.parse(content) as Record<string, string>;
const parseFile = async (path: PathLike) => {
const content = await readFile(path, 'utf8');
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 oldPlaceholderFormat = /__[a-zA-Z_]+__/g;

checkUniqueKeys(content, sourceContent, sourceFile);
const checkPlaceholdersFormat = (json: Record<string, string>, path: PathLike) => {
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);

const usedKeys = Object.entries(sourceContent)
if (outdatedKeys.length > 0) {
throw new Error(`Outdated placeholder format on file ${path}: ${outdatedKeys.map((key) => inspect(key, { colors: true })).join(', ')}`);
}
};

const checkMissingPlurals = (json: Record<string, string>, path: PathLike, lng: string) => {
const keys = Object.keys(json);

if (!i18next.isInitialized) {
i18next.init({ initImmediate: false });
}

const pluralSuffixes = i18next.services.pluralResolver.getSuffixes(lng) as string[];
const allKeys = keys.flatMap((key) => {
for (const pluralSuffix of pluralSuffixes) {
if (key.endsWith(`_${pluralSuffix}`)) {
const normalizedKey = key.slice(0, -pluralSuffix.length);
return [normalizedKey, ...pluralSuffixes.map((suffix) => `${normalizedKey}_${suffix}`)];
}
}

return [key];
});

if (keys.length !== allKeys.length) {
const missingKeys = allKeys.filter((key) => !keys.includes(key));

throw new Error(`Missing plural keys on file ${path}: ${missingKeys.map((key) => inspect(key, { colors: true })).join(', ')}`);
}
};

const checkFiles = async (sourceDirPath: string, sourceFile: string, fix = false) => {
const sourcePath = join(sourceDirPath, sourceFile);
const sourceJson = await parseFile(sourcePath);

checkPlaceholdersFormat(sourceJson, sourcePath);
checkMissingPlurals(sourceJson, sourcePath, 'en');

const i18nFiles = await fg([join(sourceDirPath, `**/*.i18n.json`), `!${sourcePath}`]);

const translations = await Promise.all(
i18nFiles.map(async (path) => ({ path, json: await parseFile(path), lng: /\/(.*).i18n.json$/.exec(path)?.[1] ?? 'FIXME' })),
);

for await (const { path, json, lng } of translations) {
checkPlaceholdersFormat(json, path);
checkMissingPlurals(json, path, lng);
}

const regexVar = /__[a-zA-Z_]+__/g;

const usedKeys = Object.entries(sourceJson)
.map(([key, value]) => {
const replaces = value.match(regexVar);
return {
Expand All @@ -88,20 +177,15 @@ const checkFiles = async (sourcePath: string, sourceFile: string, fix = false) =
})
.filter((usedKey): usedKey is { key: string; replaces: RegExpMatchArray } => !!usedKey.replaces);

const i18nFiles = await fg([`${sourcePath}/**/*.i18n.json`]);

if (fix) {
return removeMissingKeys(i18nFiles, usedKeys);
}

validate(i18nFiles, usedKeys);
};

(async () => {
try {
await checkFiles('./packages/rocketchat-i18n/i18n', 'en.i18n.json', process.argv[2] === '--fix');
} catch (e) {
console.error(e);
process.exit(1);
}
})();
const fix = process.argv[2] === '--fix';
checkFiles('./packages/rocketchat-i18n/i18n', 'en.i18n.json', fix).catch((e) => {
console.error(e);
process.exit(1);
});
4 changes: 2 additions & 2 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -5886,7 +5886,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>.",
Expand Down Expand Up @@ -6258,4 +6258,4 @@
"Seat_limit_reached": "Seat limit reached",
"Seat_limit_reached_Description": "Your workspace reached its contractual seat limit. Buy more seats to add more users.",
"Buy_more_seats": "Buy more seats"
}
}
6 changes: 3 additions & 3 deletions apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -5754,4 +5754,4 @@
"Theme_Appearence": "Teeman ulkoasu",
"Enterprise": "Yritys",
"UpgradeToGetMore_engagement-dashboard_Title": "Analytics"
}
}
4 changes: 2 additions & 2 deletions apps/meteor/packages/rocketchat-i18n/i18n/it.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -2862,4 +2862,4 @@
"registration.component.form.sendConfirmationEmail": "Invia email di conferma",
"Enterprise": "impresa",
"UpgradeToGetMore_engagement-dashboard_Title": "Analytics"
}
}
4 changes: 2 additions & 2 deletions apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "メールのフィールド名",
Expand Down Expand Up @@ -4836,4 +4836,4 @@
"Enterprise": "エンタープライズ",
"UpgradeToGetMore_engagement-dashboard_Title": "分析",
"UpgradeToGetMore_auditing_Title": "メッセージ監査"
}
}
10 changes: 5 additions & 5 deletions apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "ოთახის აღწერა წარმატებით შეიცვალა",
Expand Down Expand Up @@ -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": "თაიმაუტები",
Expand Down Expand Up @@ -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}}.",
Expand Down Expand Up @@ -3671,4 +3671,4 @@
"onboarding.form.registerOfflineForm.title": "ხელით დარეგისტრირება",
"UpgradeToGetMore_engagement-dashboard_Title": "ანალიტიკა",
"UpgradeToGetMore_auditing_Title": "შეტყობინებების შემოწმება"
}
}
Loading

0 comments on commit 82aa840

Please sign in to comment.