Skip to content

Commit c599d14

Browse files
committed
tip conflict fix
1 parent ff57c06 commit c599d14

File tree

6 files changed

+3078
-2726
lines changed

6 files changed

+3078
-2726
lines changed

.github/ci-validateTranslations.js

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
class JsonParser {
5+
constructor(text, filePath) {
6+
this.text = text;
7+
this.filePath = filePath;
8+
this.index = 0;
9+
this.duplicateKeys = [];
10+
}
11+
12+
parse() {
13+
const value = this.parseValue([]);
14+
this.skipWhitespace();
15+
if (this.index !== this.text.length) {
16+
this.throwError("Unexpected trailing content");
17+
}
18+
return { value, duplicateKeys: this.duplicateKeys };
19+
}
20+
21+
parseValue(pathStack) {
22+
this.skipWhitespace();
23+
const char = this.peek();
24+
if (char === "{") {
25+
return this.parseObject(pathStack);
26+
}
27+
if (char === "[") {
28+
return this.parseArray(pathStack);
29+
}
30+
if (char === "\"") {
31+
return this.parseString();
32+
}
33+
if (char === "t") {
34+
return this.parseLiteral("true", true);
35+
}
36+
if (char === "f") {
37+
return this.parseLiteral("false", false);
38+
}
39+
if (char === "n") {
40+
return this.parseLiteral("null", null);
41+
}
42+
if (char === "-" || this.isDigit(char)) {
43+
return this.parseNumber();
44+
}
45+
this.throwError("Unexpected token");
46+
}
47+
48+
parseObject(pathStack) {
49+
this.consume("{");
50+
this.skipWhitespace();
51+
const result = {};
52+
const keySet = new Set();
53+
54+
if (this.peek() === "}") {
55+
this.index += 1;
56+
return result;
57+
}
58+
59+
while (true) {
60+
this.skipWhitespace();
61+
if (this.peek() !== "\"") {
62+
this.throwError("Expected string key");
63+
}
64+
const key = this.parseString();
65+
if (keySet.has(key)) {
66+
this.duplicateKeys.push({ path: [...pathStack, key] });
67+
} else {
68+
keySet.add(key);
69+
}
70+
71+
this.skipWhitespace();
72+
this.consume(":");
73+
const value = this.parseValue([...pathStack, key]);
74+
result[key] = value;
75+
this.skipWhitespace();
76+
77+
const next = this.peek();
78+
if (next === "}") {
79+
this.index += 1;
80+
break;
81+
}
82+
this.consume(",");
83+
}
84+
85+
return result;
86+
}
87+
88+
parseArray(pathStack) {
89+
this.consume("[");
90+
this.skipWhitespace();
91+
const result = [];
92+
if (this.peek() === "]") {
93+
this.index += 1;
94+
return result;
95+
}
96+
97+
let index = 0;
98+
while (true) {
99+
const value = this.parseValue([...pathStack, index]);
100+
result.push(value);
101+
index += 1;
102+
this.skipWhitespace();
103+
104+
const next = this.peek();
105+
if (next === "]") {
106+
this.index += 1;
107+
break;
108+
}
109+
this.consume(",");
110+
}
111+
112+
return result;
113+
}
114+
115+
parseString() {
116+
this.consume("\"");
117+
let result = "";
118+
119+
while (this.index < this.text.length) {
120+
const char = this.text[this.index];
121+
if (char === "\"") {
122+
this.index += 1;
123+
return result;
124+
}
125+
if (char === "\\") {
126+
this.index += 1;
127+
const escaped = this.text[this.index];
128+
if (escaped === undefined) {
129+
this.throwError("Unterminated escape sequence");
130+
}
131+
if (escaped === "\"" || escaped === "\\" || escaped === "/") {
132+
result += escaped;
133+
} else if (escaped === "b") {
134+
result += "\b";
135+
} else if (escaped === "f") {
136+
result += "\f";
137+
} else if (escaped === "n") {
138+
result += "\n";
139+
} else if (escaped === "r") {
140+
result += "\r";
141+
} else if (escaped === "t") {
142+
result += "\t";
143+
} else if (escaped === "u") {
144+
const hex = this.text.slice(this.index + 1, this.index + 5);
145+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
146+
this.throwError("Invalid unicode escape");
147+
}
148+
result += String.fromCharCode(parseInt(hex, 16));
149+
this.index += 4;
150+
} else {
151+
this.throwError("Invalid escape sequence");
152+
}
153+
this.index += 1;
154+
continue;
155+
}
156+
if (char < " ") {
157+
this.throwError("Unescaped control character in string");
158+
}
159+
result += char;
160+
this.index += 1;
161+
}
162+
163+
this.throwError("Unterminated string");
164+
}
165+
166+
parseNumber() {
167+
const remaining = this.text.slice(this.index);
168+
const match = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(remaining);
169+
if (!match) {
170+
this.throwError("Invalid number");
171+
}
172+
this.index += match[0].length;
173+
return Number(match[0]);
174+
}
175+
176+
parseLiteral(literal, value) {
177+
if (this.text.slice(this.index, this.index + literal.length) !== literal) {
178+
this.throwError(`Expected ${literal}`);
179+
}
180+
this.index += literal.length;
181+
return value;
182+
}
183+
184+
skipWhitespace() {
185+
while (this.index < this.text.length) {
186+
const char = this.text[this.index];
187+
if (char === " " || char === "\n" || char === "\r" || char === "\t") {
188+
this.index += 1;
189+
continue;
190+
}
191+
break;
192+
}
193+
}
194+
195+
peek() {
196+
return this.text[this.index];
197+
}
198+
199+
consume(expected) {
200+
if (this.text[this.index] !== expected) {
201+
this.throwError(`Expected '${expected}'`);
202+
}
203+
this.index += 1;
204+
}
205+
206+
isDigit(char) {
207+
return char >= "0" && char <= "9";
208+
}
209+
210+
throwError(message) {
211+
const { line, column } = this.getLineColumn(this.index);
212+
const error = new Error(`${message} at ${this.filePath}:${line}:${column}`);
213+
throw error;
214+
}
215+
216+
getLineColumn(position) {
217+
const slice = this.text.slice(0, position);
218+
const lines = slice.split("\n");
219+
return { line: lines.length, column: lines[lines.length - 1].length + 1 };
220+
}
221+
}
222+
223+
function formatPath(pathParts) {
224+
return pathParts.reduce((acc, part) => {
225+
if (typeof part === "number") {
226+
return `${acc}[${part}]`;
227+
}
228+
return acc ? `${acc}.${part}` : part;
229+
}, "");
230+
}
231+
232+
function validateTranslationFiles() {
233+
const translationsDir = path.join(__dirname, "..", "translations");
234+
const files = fs
235+
.readdirSync(translationsDir)
236+
.filter((file) => file.endsWith(".json"))
237+
.sort();
238+
239+
const errors = [];
240+
241+
for (const file of files) {
242+
const filePath = path.join(translationsDir, file);
243+
const content = fs.readFileSync(filePath, "utf8");
244+
245+
try {
246+
const parser = new JsonParser(content, filePath);
247+
const { duplicateKeys } = parser.parse();
248+
if (duplicateKeys.length > 0) {
249+
duplicateKeys.forEach((entry) => {
250+
errors.push(
251+
`Duplicate key '${formatPath(entry.path)}' in ${filePath}`
252+
);
253+
});
254+
}
255+
} catch (error) {
256+
errors.push(error.message);
257+
}
258+
}
259+
260+
if (errors.length > 0) {
261+
console.error("Translation validation failed:\n");
262+
errors.forEach((error) => console.error(`- ${error}`));
263+
process.exit(1);
264+
}
265+
266+
console.log("Translation validation passed.");
267+
}
268+
269+
validateTranslationFiles();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Validate translations
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
paths:
8+
- "translations/*.json"
9+
pull_request:
10+
branches:
11+
- develop
12+
paths:
13+
- "translations/*.json"
14+
15+
jobs:
16+
validate:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Check out repository
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Node
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: "20"
27+
28+
- name: Validate translations
29+
run: node .github/ci-validateTranslations.js

index.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848

4949
<meta name="msapplication-TileColor" content="#da532c" />
5050
<meta name="theme-color" content="#0f131d" />
51-
<link rel="stylesheet" href="./main.css?ver=4034" />
51+
<link rel="stylesheet" href="./main.css?ver=4035" />
5252
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.js"></script>
5353

5454
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
@@ -514,9 +514,14 @@ <h2 id="add_camera">
514514
</div>
515515
<div id="guestTips" style="display:none" aria-hidden="true">
516516
<p data-translate="for-the-best-possible-experience-make-sure">For the best possible experience, make sure</p>
517-
<span><i class="las la-plug"></i><span data-translate="your-device-is-powered">Your device is powered</span></span>
518-
<span><i class="las la-ethernet"></i><span data-translate="your-connection-is-hardwired-instead-of-wifi">Your connection is hardwired instead of wifi</span></span>
519-
<span><i class="las la-headphones"></i><span data-translate="you-are-using-headphones-earphones">You are using headphones / earphones</span></span>
517+
<ul class="guest-tip-list">
518+
<li class="guest-tip-item" data-tip-id="1" data-tip-default="true"><i class="las la-plug"></i><span data-translate="your-device-is-powered">Your device is powered</span></li>
519+
<li class="guest-tip-item" data-tip-id="2" data-tip-default="true"><i class="las la-ethernet"></i><span data-translate="your-connection-is-hardwired-instead-of-wifi">Your connection is hardwired instead of wifi</span></li>
520+
<li class="guest-tip-item" data-tip-id="3" data-tip-default="true"><i class="las la-headphones"></i><span data-translate="you-are-using-headphones-earphones">You are using headphones / earphones</span></li>
521+
<li class="guest-tip-item" data-tip-id="4"><i class="las la-times-circle"></i><span data-translate="close-other-video-apps">Close other video/calling apps</span></li>
522+
<li class="guest-tip-item" data-tip-id="5"><i class="las la-volume-mute"></i><span data-translate="use-quiet-room">Use a quiet room</span></li>
523+
<li class="guest-tip-item" data-tip-id="6"><i class="las la-sun"></i><span data-translate="face-good-lighting">Face a light source</span></li>
524+
</ul>
520525
</div>
521526
<div id="videoMenu" class="videoMenu" aria-hidden="true">
522527
<div class="title">

0 commit comments

Comments
 (0)