poly-lexis 0.5.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -532,6 +532,21 @@ var init_language_fallback = __esm({
532
532
  // src/translations/utils/utils.ts
533
533
  import * as fs from "fs";
534
534
  import * as path2 from "path";
535
+ function flattenObject(obj, prefix = "") {
536
+ const result = {};
537
+ for (const [key, value] of Object.entries(obj)) {
538
+ const newKey = prefix ? `${prefix}.${key}` : key;
539
+ if (typeof value === "string") {
540
+ result[newKey] = value;
541
+ } else if (typeof value === "object" && value !== null) {
542
+ Object.assign(result, flattenObject(value, newKey));
543
+ }
544
+ }
545
+ return result;
546
+ }
547
+ function isNestedObject(obj) {
548
+ return Object.values(obj).some((value) => typeof value === "object" && value !== null);
549
+ }
535
550
  function readTranslations(translationsPath, language) {
536
551
  const langPath = path2.join(translationsPath, language);
537
552
  if (!fs.existsSync(langPath)) {
@@ -543,7 +558,12 @@ function readTranslations(translationsPath, language) {
543
558
  const namespace = path2.basename(file, ".json");
544
559
  const filePath = path2.join(langPath, file);
545
560
  const content = fs.readFileSync(filePath, "utf-8");
546
- translations[namespace] = JSON.parse(content);
561
+ const parsed = JSON.parse(content);
562
+ if (isNestedObject(parsed)) {
563
+ translations[namespace] = flattenObject(parsed);
564
+ } else {
565
+ translations[namespace] = parsed;
566
+ }
547
567
  }
548
568
  return translations;
549
569
  }
@@ -832,11 +852,28 @@ var init_init = __esm({
832
852
  // src/translations/cli/generate-types.ts
833
853
  var generate_types_exports = {};
834
854
  __export(generate_types_exports, {
855
+ extractPluralBaseKeys: () => extractPluralBaseKeys,
835
856
  generateTranslationTypes: () => generateTranslationTypes
836
857
  });
837
858
  import { execSync } from "child_process";
838
859
  import * as fs3 from "fs";
839
860
  import * as path4 from "path";
861
+ function extractPluralBaseKeys(keys) {
862
+ const keySet = new Set(keys);
863
+ const baseKeys = /* @__PURE__ */ new Set();
864
+ for (const key of keys) {
865
+ for (const suffix of PLURAL_SUFFIXES) {
866
+ if (key.endsWith(suffix)) {
867
+ const baseKey = key.slice(0, -suffix.length);
868
+ if (baseKey && !keySet.has(baseKey)) {
869
+ baseKeys.add(baseKey);
870
+ }
871
+ break;
872
+ }
873
+ }
874
+ }
875
+ return Array.from(baseKeys);
876
+ }
840
877
  function generateTranslationTypes(projectRoot = process.cwd()) {
841
878
  console.log("=====");
842
879
  console.time("i18n types generated");
@@ -860,6 +897,8 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
860
897
  const keys = Object.keys(translations[namespace] || {});
861
898
  allKeys = allKeys.concat(keys);
862
899
  }
900
+ const pluralBaseKeys = extractPluralBaseKeys(allKeys);
901
+ allKeys = allKeys.concat(pluralBaseKeys);
863
902
  const outputDir = path4.dirname(outputFilePath);
864
903
  if (!fs3.existsSync(outputDir)) {
865
904
  fs3.mkdirSync(outputDir, { recursive: true });
@@ -879,13 +918,14 @@ function generateTranslationTypes(projectRoot = process.cwd()) {
879
918
  console.timeEnd("i18n types generated");
880
919
  console.log("=====");
881
920
  }
882
- var typeTemplate;
921
+ var PLURAL_SUFFIXES, typeTemplate;
883
922
  var init_generate_types = __esm({
884
923
  "src/translations/cli/generate-types.ts"() {
885
924
  "use strict";
886
925
  init_esm_shims();
887
926
  init_utils();
888
927
  init_init();
928
+ PLURAL_SUFFIXES = ["_zero", "_one", "_two", "_few", "_many", "_other"];
889
929
  typeTemplate = (translationKeys, namespaceKeys) => `
890
930
  export const translationKeys = [${translationKeys.map((key) => `"${key}"`).join(", ")}] as const;
891
931
  export const namespaceKeys = [${namespaceKeys.map((key) => `"${key}"`).join(", ")}] as const;
@@ -1178,11 +1218,97 @@ var init_find_unused = __esm({
1178
1218
  }
1179
1219
  });
1180
1220
 
1221
+ // src/translations/cli/find-duplicates.ts
1222
+ var find_duplicates_exports = {};
1223
+ __export(find_duplicates_exports, {
1224
+ findDuplicates: () => findDuplicates,
1225
+ printDuplicateKeysResult: () => printDuplicateKeysResult
1226
+ });
1227
+ import * as path11 from "path";
1228
+ function findDuplicates(projectRoot = process.cwd()) {
1229
+ const config = loadConfig(projectRoot);
1230
+ const translationsPath = path11.join(projectRoot, config.translationsPath);
1231
+ const sourceLanguage = config.sourceLanguage;
1232
+ const namespaces = getNamespaces(translationsPath, sourceLanguage);
1233
+ const sourceTranslations = readTranslations(translationsPath, sourceLanguage);
1234
+ console.log("=====");
1235
+ console.log("Finding duplicate translations (common namespace)");
1236
+ console.log("=====");
1237
+ console.log(`Source language: ${sourceLanguage}`);
1238
+ console.log(`Namespaces: ${namespaces.join(", ")}`);
1239
+ console.log("=====");
1240
+ const commonValues = sourceTranslations["common"] || {};
1241
+ if (Object.keys(commonValues).length === 0) {
1242
+ console.log("No common namespace found or it is empty.");
1243
+ return { duplicates: [], totalKeysChecked: 0 };
1244
+ }
1245
+ const valueToCommonKey = /* @__PURE__ */ new Map();
1246
+ for (const [key, value] of Object.entries(commonValues)) {
1247
+ if (!valueToCommonKey.has(value)) {
1248
+ valueToCommonKey.set(value, key);
1249
+ }
1250
+ }
1251
+ const duplicates = [];
1252
+ let totalKeysChecked = 0;
1253
+ for (const namespace of namespaces) {
1254
+ if (namespace === "common") continue;
1255
+ const nsValues = sourceTranslations[namespace] || {};
1256
+ for (const [key, value] of Object.entries(nsValues)) {
1257
+ totalKeysChecked++;
1258
+ const commonKey = valueToCommonKey.get(value);
1259
+ if (commonKey) {
1260
+ duplicates.push({
1261
+ namespace,
1262
+ key,
1263
+ commonKey,
1264
+ value
1265
+ });
1266
+ }
1267
+ }
1268
+ }
1269
+ console.log("=====");
1270
+ return { duplicates, totalKeysChecked };
1271
+ }
1272
+ function printDuplicateKeysResult(result) {
1273
+ if (result.duplicates.length === 0) {
1274
+ console.log("\u2713 No duplicate values found across namespaces!");
1275
+ return;
1276
+ }
1277
+ console.log(`
1278
+ \u26A0 Found ${result.duplicates.length} values duplicated from common:
1279
+ `);
1280
+ const byNamespace = /* @__PURE__ */ new Map();
1281
+ for (const dup of result.duplicates) {
1282
+ const items = byNamespace.get(dup.namespace) || [];
1283
+ items.push(dup);
1284
+ byNamespace.set(dup.namespace, items);
1285
+ }
1286
+ for (const [namespace, items] of byNamespace) {
1287
+ console.log(` ${namespace}:`);
1288
+ for (const item of items) {
1289
+ console.log(` - ${item.key} \u2192 common.${item.commonKey} ("${item.value}")`);
1290
+ }
1291
+ }
1292
+ console.log(`
1293
+ \u{1F4CA} Summary:`);
1294
+ console.log(` Keys checked: ${result.totalKeysChecked}`);
1295
+ console.log(` Duplicates found: ${result.duplicates.length}`);
1296
+ console.log("=====");
1297
+ }
1298
+ var init_find_duplicates = __esm({
1299
+ "src/translations/cli/find-duplicates.ts"() {
1300
+ "use strict";
1301
+ init_esm_shims();
1302
+ init_utils();
1303
+ init_init();
1304
+ }
1305
+ });
1306
+
1181
1307
  // src/cli/translations.ts
1182
1308
  init_esm_shims();
1183
1309
  import "dotenv/config";
1184
1310
  import * as fs7 from "fs";
1185
- import * as path11 from "path";
1311
+ import * as path12 from "path";
1186
1312
  import { parseArgs } from "util";
1187
1313
  import { confirm as confirm2, input as input2, select as select2 } from "@inquirer/prompts";
1188
1314
 
@@ -2088,9 +2214,10 @@ Smart translation management - automatically handles initialization, validation,
2088
2214
  auto-filling, and type generation based on your project's current state.
2089
2215
 
2090
2216
  Commands:
2091
- (none) Smart mode - validates, fills, and generates types
2092
- add Add a new translation key
2093
- find-unused Find translation keys that are not used in the codebase
2217
+ (none) Smart mode - validates, fills, and generates types
2218
+ add Add a new translation key
2219
+ find-unused Find translation keys that are not used in the codebase
2220
+ find-duplicates Find values duplicated from the common namespace
2094
2221
 
2095
2222
  Options (Smart Mode):
2096
2223
  -a, --auto-fill Auto-fill missing translations with DeepL or Google Translate
@@ -2140,6 +2267,9 @@ Examples:
2140
2267
  # Find unused translation keys
2141
2268
  translations find-unused
2142
2269
 
2270
+ # Find values duplicated from common namespace
2271
+ translations find-duplicates
2272
+
2143
2273
  What happens in smart mode:
2144
2274
  1. Checks if translations are initialized (creates .translationsrc.json if needed)
2145
2275
  2. Validates all translations against source language
@@ -2162,12 +2292,24 @@ if (command === "find-unused") {
2162
2292
  process.exit(1);
2163
2293
  }
2164
2294
  })();
2295
+ } else if (command === "find-duplicates") {
2296
+ (async () => {
2297
+ try {
2298
+ const { findDuplicates: findDuplicates2, printDuplicateKeysResult: printDuplicateKeysResult2 } = await Promise.resolve().then(() => (init_find_duplicates(), find_duplicates_exports));
2299
+ console.log("\n\u{1F50D} Finding duplicate translations (common namespace)...\n");
2300
+ const result = findDuplicates2(process.cwd());
2301
+ printDuplicateKeysResult2(result);
2302
+ } catch (error) {
2303
+ console.error("Error:", error instanceof Error ? error.message : error);
2304
+ process.exit(1);
2305
+ }
2306
+ })();
2165
2307
  } else if (command === "add") {
2166
2308
  if (!values.namespace && !values.key && !values.value) {
2167
2309
  (async () => {
2168
2310
  try {
2169
2311
  console.log("\n\u2728 Add a new translation key\n");
2170
- const configPath = path11.join(process.cwd(), ".translationsrc.json");
2312
+ const configPath = path12.join(process.cwd(), ".translationsrc.json");
2171
2313
  const isInitialized = fs7.existsSync(configPath);
2172
2314
  if (!isInitialized) {
2173
2315
  console.log("\u26A0\uFE0F Translation structure not initialized.");
@@ -2184,7 +2326,7 @@ if (command === "find-unused") {
2184
2326
  }
2185
2327
  }
2186
2328
  const config = loadConfig(process.cwd());
2187
- const translationsPath = path11.join(process.cwd(), config.translationsPath);
2329
+ const translationsPath = path12.join(process.cwd(), config.translationsPath);
2188
2330
  const existingNamespaces = getNamespaces(translationsPath, config.sourceLanguage);
2189
2331
  let namespace;
2190
2332
  if (existingNamespaces.length > 0) {
@@ -2307,7 +2449,7 @@ if (command === "find-unused") {
2307
2449
  } else {
2308
2450
  const hasFlags = values["auto-fill"] || values.language || values["skip-types"] || values["dry-run"];
2309
2451
  if (hasFlags) {
2310
- const configPath = path11.join(process.cwd(), ".translationsrc.json");
2452
+ const configPath = path12.join(process.cwd(), ".translationsrc.json");
2311
2453
  const config = fs7.existsSync(configPath) ? loadConfig(process.cwd()) : { provider: "deepl" };
2312
2454
  const provider = config.provider || "deepl";
2313
2455
  const envVarName = provider === "google" ? "GOOGLE_TRANSLATE_API_KEY" : "DEEPL_API_KEY";
@@ -2333,7 +2475,7 @@ if (command === "find-unused") {
2333
2475
  } else {
2334
2476
  (async () => {
2335
2477
  try {
2336
- const configPath = path11.join(process.cwd(), ".translationsrc.json");
2478
+ const configPath = path12.join(process.cwd(), ".translationsrc.json");
2337
2479
  const isInitialized = fs7.existsSync(configPath);
2338
2480
  console.log("\n\u{1F30D} Translation Management\n");
2339
2481
  const action = await select2({
@@ -2354,6 +2496,11 @@ if (command === "find-unused") {
2354
2496
  value: "find-unused",
2355
2497
  description: "Find translation keys not used in the codebase"
2356
2498
  },
2499
+ {
2500
+ name: "\u{1F50E} Find duplicate values",
2501
+ value: "find-duplicates",
2502
+ description: "Find values duplicated from the common namespace"
2503
+ },
2357
2504
  {
2358
2505
  name: "\u{1F916} Auto-fill missing translations",
2359
2506
  value: "autofill",
@@ -2393,7 +2540,7 @@ if (command === "find-unused") {
2393
2540
  }
2394
2541
  }
2395
2542
  const config = loadConfig(process.cwd());
2396
- const translationsPath = path11.join(process.cwd(), config.translationsPath);
2543
+ const translationsPath = path12.join(process.cwd(), config.translationsPath);
2397
2544
  const existingNamespaces = getNamespaces(translationsPath, config.sourceLanguage);
2398
2545
  let namespace;
2399
2546
  if (existingNamespaces.length > 0) {
@@ -2503,6 +2650,10 @@ if (command === "find-unused") {
2503
2650
  const { findUnusedKeys: findUnusedKeys2, printUnusedKeysResult: printUnusedKeysResult2 } = await Promise.resolve().then(() => (init_find_unused(), find_unused_exports));
2504
2651
  const result = findUnusedKeys2(process.cwd());
2505
2652
  printUnusedKeysResult2(result);
2653
+ } else if (action === "find-duplicates") {
2654
+ const { findDuplicates: findDuplicates2, printDuplicateKeysResult: printDuplicateKeysResult2 } = await Promise.resolve().then(() => (init_find_duplicates(), find_duplicates_exports));
2655
+ const result = findDuplicates2(process.cwd());
2656
+ printDuplicateKeysResult2(result);
2506
2657
  } else if (action === "types") {
2507
2658
  console.log("\u{1F4DD} Generating TypeScript types...\n");
2508
2659
  const { generateTranslationTypes: generateTranslationTypes2 } = await Promise.resolve().then(() => (init_generate_types(), generate_types_exports));