Repository navigation
Expand file tree
/
Copy pathi18n.node.js
More file actions
204 lines (165 loc) · 7.48 KB
/
Copy pathi18n.node.js
File metadata and controls
204 lines (165 loc) · 7.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
const fs = require('fs');
const path = require('path');
let reservedWords = require('./reserved18n.json'); // Load reserved words from JSON file
const targetDir = 'src'; // Define the starting directory
// const targetDir = 'research'; // Define the starting directory
const namespace = 'fluent-security'; // Define the namespace for the translation strings
const finalFile = 'app/Services/TransStrings.php'; // Define the file to replace the translation strings
function modifyAndReconstructSprintf(s) {
// Extract the entire sprintf call excluding 'sprintf(' and the closing ')'
const fullPattern = /sprintf\((.*)\)$/;
const fullMatch = s.match(fullPattern);
if (!fullMatch) {
return null; // Return null if the format doesn't match expected
}
// Split the entire content by ', ' but only outside of quotes to avoid breaking URLs and functions
const args = [];
let depth = 0;
let lastIndex = 0;
for (let i = 0; i < fullMatch[1].length; i++) {
const char = fullMatch[1][i];
if (char === '(') depth++;
if (char === ')') depth--;
if (char === '\'' && fullMatch[1][i - 1] !== '\\') {
// Toggle in or out of a single quote
depth = depth === 0 ? 1000 : 0;
}
if (char === ',' && depth === 0) {
args.push(fullMatch[1].substring(lastIndex, i).trim());
lastIndex = i + 1;
}
}
// Add the last argument
args.push(fullMatch[1].substring(lastIndex).trim());
if (args.length === 0) {
return null; // No arguments found, unlikely scenario
}
// Replace escaped single quotes in the first argument
args[0] = args[0].replace(/\\'/g, "'");
// Reconstruct the full sprintf string using modified arguments
return `sprintf(${args.map(arg => arg).join(', ')})`;
}
/*
* Strings that are translated where they are *used* rather than where they are written.
*
* A handful of screens keep their labels in a plain data structure - the route table, the
* section bar, the colour groups on the email template screen - and render them through
* `$t(item.title)`. That call is correct, but the string it looks up never appears inside
* a `$t()` in the source, so the pass below could not see it and the lookup quietly fell
* back to English. Whether such a string was translated came down to whether the same
* words happened to be written out literally on some other screen.
*
* So each of those files names the keys whose literal values are shown to somebody. Adding
* a route or a section picks its title up on the next run; nothing has to be listed by hand.
*/
const dynamicLabelSources = {
'src/admin/routes.js': ['title'],
'src/admin/Bits/subNav.js': ['label', 'title'],
'src/admin/Bits/searchIndex.js': ['group'],
'src/admin/Components/CustomWpEmails/TemplateSettings.vue': ['title', 'note', 'label', 'hint']
};
// Collect `key: 'literal'` for the declared keys of the declared files.
function extractDynamicLabels() {
const results = {};
for (const [file, keys] of Object.entries(dynamicLabelSources)) {
if (!fs.existsSync(file)) {
console.warn('i18n: ' + file + ' is listed as a label source but does not exist');
continue;
}
const content = fs.readFileSync(file, 'utf8');
keys.forEach(key => {
const regex = new RegExp(key + "\\s*:\\s*'((?:[^'\\\\]|\\\\.)*)'", 'g');
let match;
while ((match = regex.exec(content)) !== null) {
const value = match[1];
// Route names, slugs and other identifiers share these keys; words do not.
if (!/[A-Z]/.test(value) && !/\s/.test(value)) {
continue;
}
results[value] = true;
}
});
}
return Object.keys(results);
}
// Function to read directory contents recursively
function readDirRecursively(dir, allFiles = []) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filepath = path.join(dir, file);
if (fs.statSync(filepath).isDirectory()) {
readDirRecursively(filepath, allFiles);
} else if (path.extname(file) === '.vue' || path.extname(file) === '.js') { // Check for .vue and .js files
allFiles.push(filepath);
}
});
return allFiles;
}
// Function to extract strings from $t() in file content
function extractStrings(files) {
const results = {};
// Updated regex to capture the first argument of $t() with multiple arguments
const regex = /\$t\(\s*['"]([^'"]*?(?:\\['"][^'"]*?)*?)['"]\s*(?:,\s*[^)]*)?\)/g;
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
let match;
while ((match = regex.exec(content)) !== null) {
results[match[1]] = true; // Use the match as a key to avoid duplicates
}
});
// Extract the strings from $_n('string 1', 'string 2', var) calls
// `\s*` after the paren so a call broken across lines is found too - the house style
// for a long plural pair is one string per line, and those were silently being skipped.
const nRegex = /\$_n\(\s*['"]([^'"]*?(?:\\['"][^'"]*?)*?)['"],\s*['"]([^'"]*?(?:\\['"][^'"]*?)*?)['"]/g;
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
let match;
while ((match = nRegex.exec(content)) !== null) {
results[match[1]] = true; // Use the match as a key to avoid duplicates
results[match[2]] = true; // Use the match as a key to avoid duplicates
}
});
return Object.keys(results); // Return unique strings only
}
// Write results to a text file in PHP array format
function writeResults(strings) {
// add reservedWords if not exist
for (const key in reservedWords) {
if (!strings.includes(key)) {
strings.push(key);
}
}
// Escape strings for safe interpolation into PHP single-quoted literals.
// Source-extracted strings may contain pre-escaped sequences (e.g. \'), while
// reserved18n.json values are plain English. Normalize, then escape uniformly.
const phpEscape = s => s
.replace(/\\(['"])/g, '$1') // unescape any \' or \"
.replace(/\\/g, '\\\\') // escape remaining backslashes
.replace(/'/g, "\\'"); // escape single quotes
const sortedStrings = strings.sort(); // Sort strings in ascending order
const formattedStrings = sortedStrings.map((str) => {
const key = phpEscape(str);
const value = reservedWords[str] ? phpEscape(reservedWords[str]) : key;
return ` '${key}' => __('${value}', '${namespace}')`;
}).join(",\n");
const finalData = "<?php \n\nnamespace FluentAuth\\App\\Services;\n\n//This is a auto-generated file. Please do not modify\nclass TransStrings\n{\n public static function getStrings()\n {\n return [\n" + formattedStrings + "\n];\n }\n}";
fs.writeFile(finalFile, finalData, err => {
if (err) {
console.error('Error writing to file:', err);
} else {
console.log('Saved translation strings to ' + finalFile);
}
});
}
// Main process function
function processVueFiles() {
const vueFiles = readDirRecursively(targetDir);
const uniqueStrings = extractStrings(vueFiles);
extractDynamicLabels().forEach(label => {
if (!uniqueStrings.includes(label)) {
uniqueStrings.push(label);
}
});
writeResults(uniqueStrings);
}
processVueFiles();