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.
- package/dist/cli/translations.js +162 -11
- package/dist/cli/translations.js.map +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +60 -1
- package/dist/index.js.map +1 -1
- package/dist/scripts/verify-translations.js +21 -1
- package/dist/scripts/verify-translations.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/translations.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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)
|
|
2092
|
-
add
|
|
2093
|
-
find-unused
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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));
|