Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
4190c95b84
commit
b6cddc21cd
@ -38,8 +38,9 @@ struct AlarmRowView: View {
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
String(
|
String(
|
||||||
localized: "alarm.row.sound_prefix",
|
format: String(localized: "alarm.row.sound_prefix", defaultValue: "• %1$@"),
|
||||||
defaultValue: "• \(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))"
|
locale: .current,
|
||||||
|
AlarmSoundService.shared.getSoundDisplayName(alarm.soundName)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@ -80,8 +81,10 @@ struct AlarmRowView: View {
|
|||||||
.accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)")
|
.accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)")
|
||||||
.accessibilityLabel(
|
.accessibilityLabel(
|
||||||
String(
|
String(
|
||||||
localized: "alarm.row.accessibility_label",
|
format: String(localized: "alarm.row.accessibility_label", defaultValue: "%1$@, %2$@"),
|
||||||
defaultValue: "\(alarm.label), \(alarm.formattedTime())"
|
locale: .current,
|
||||||
|
alarm.label,
|
||||||
|
alarm.formattedTime()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.accessibilityValue(
|
.accessibilityValue(
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Bedrock
|
import Bedrock
|
||||||
|
import Foundation
|
||||||
|
|
||||||
/// View for selecting snooze duration
|
/// View for selecting snooze duration
|
||||||
struct SnoozeSelectionView: View {
|
struct SnoozeSelectionView: View {
|
||||||
@ -22,8 +23,9 @@ struct SnoozeSelectionView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text(
|
Text(
|
||||||
String(
|
String(
|
||||||
localized: "alarm.snooze.duration_minutes",
|
format: String(localized: "alarm.snooze.duration_minutes", defaultValue: "%lld minutes"),
|
||||||
defaultValue: "\(duration) minutes"
|
locale: .current,
|
||||||
|
duration
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.foregroundStyle(AppTextColors.primary)
|
.foregroundStyle(AppTextColors.primary)
|
||||||
|
|||||||
@ -196,8 +196,11 @@ struct ClockView: View {
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
String(
|
String(
|
||||||
localized: "clock.debug.orientation",
|
format: String(localized: "clock.debug.orientation", defaultValue: "Orientation: %1$@"),
|
||||||
defaultValue: "Orientation: \(isLandscape ? "Landscape" : "Portrait")"
|
locale: .current,
|
||||||
|
isLandscape
|
||||||
|
? String(localized: "clock.debug.orientation.landscape", defaultValue: "Landscape")
|
||||||
|
: String(localized: "clock.debug.orientation.portrait", defaultValue: "Portrait")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -125,7 +125,7 @@ final class TheNoiseClockUITests: XCTestCase {
|
|||||||
usleep(250_000)
|
usleep(250_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
let playButton = app.buttons["Play Sound"]
|
let playButton = app.buttons["noise.playStopButton"]
|
||||||
if playButton.waitForExistence(timeout: 4) {
|
if playButton.waitForExistence(timeout: 4) {
|
||||||
playButton.tap()
|
playButton.tap()
|
||||||
}
|
}
|
||||||
@ -192,22 +192,17 @@ final class TheNoiseClockUITests: XCTestCase {
|
|||||||
openTab(named: "Alarms", in: app)
|
openTab(named: "Alarms", in: app)
|
||||||
|
|
||||||
// If no alarms exist, create one with defaults.
|
// If no alarms exist, create one with defaults.
|
||||||
if app.staticTexts["No Alarms Set"].exists || app.buttons["Add Your First Alarm"].exists {
|
let firstSwitch = app.switches.firstMatch
|
||||||
let addFirstAlarm = app.buttons["Add Your First Alarm"]
|
if !firstSwitch.waitForExistence(timeout: 2) {
|
||||||
if addFirstAlarm.exists {
|
tapNavigationPlusButton(in: app)
|
||||||
addFirstAlarm.tap()
|
|
||||||
} else {
|
|
||||||
tapNavigationPlusButton(in: app)
|
|
||||||
}
|
|
||||||
|
|
||||||
let saveButton = app.buttons["Save"]
|
let saveButton = app.buttons["alarms.add.saveButton"]
|
||||||
XCTAssertTrue(saveButton.waitForExistence(timeout: 5), "Add Alarm sheet did not appear.")
|
XCTAssertTrue(saveButton.waitForExistence(timeout: 5), "Add Alarm sheet did not appear.")
|
||||||
saveButton.tap()
|
saveButton.tap()
|
||||||
sleep(1)
|
sleep(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure at least one alarm is enabled.
|
// Make sure at least one alarm is enabled.
|
||||||
let firstSwitch = app.switches.firstMatch
|
|
||||||
if firstSwitch.waitForExistence(timeout: 3), !isSwitchOn(firstSwitch) {
|
if firstSwitch.waitForExistence(timeout: 3), !isSwitchOn(firstSwitch) {
|
||||||
firstSwitch.tap()
|
firstSwitch.tap()
|
||||||
sleep(1)
|
sleep(1)
|
||||||
@ -292,6 +287,11 @@ final class TheNoiseClockUITests: XCTestCase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dismissInterferingPromptIfPresent(in: app)
|
||||||
|
if tapLabeledButton() || tapIndexedButton() || tapLabeledButtonAnywhere() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let buttonLabels = app.buttons.allElementsBoundByIndex
|
let buttonLabels = app.buttons.allElementsBoundByIndex
|
||||||
.prefix(16)
|
.prefix(16)
|
||||||
.map(\.label)
|
.map(\.label)
|
||||||
@ -302,6 +302,7 @@ final class TheNoiseClockUITests: XCTestCase {
|
|||||||
@MainActor
|
@MainActor
|
||||||
private func tapNavigationPlusButton(in app: XCUIApplication) {
|
private func tapNavigationPlusButton(in app: XCUIApplication) {
|
||||||
let plusCandidates = [
|
let plusCandidates = [
|
||||||
|
app.buttons["alarms.addButton"],
|
||||||
app.navigationBars.buttons["plus"],
|
app.navigationBars.buttons["plus"],
|
||||||
app.navigationBars.buttons["Add"],
|
app.navigationBars.buttons["Add"],
|
||||||
app.buttons["plus"]
|
app.buttons["plus"]
|
||||||
@ -315,6 +316,17 @@ final class TheNoiseClockUITests: XCTestCase {
|
|||||||
XCTFail("Could not find add (+) button.")
|
XCTFail("Could not find add (+) button.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func dismissInterferingPromptIfPresent(in app: XCUIApplication) {
|
||||||
|
guard app.tabBars.count == 0 else { return }
|
||||||
|
let buttons = app.buttons.allElementsBoundByIndex.filter { $0.exists && $0.isHittable }
|
||||||
|
guard buttons.count == 2 else { return }
|
||||||
|
|
||||||
|
// Keep Awake prompt can obscure tabs in some locale/device combinations.
|
||||||
|
buttons[1].tap()
|
||||||
|
usleep(300_000)
|
||||||
|
}
|
||||||
|
|
||||||
private func isSwitchOn(_ toggle: XCUIElement) -> Bool {
|
private func isSwitchOn(_ toggle: XCUIElement) -> Bool {
|
||||||
guard let value = toggle.value as? String else { return false }
|
guard let value = toggle.value as? String else { return false }
|
||||||
return value == "1" || value.lowercased() == "on"
|
return value == "1" || value.lowercased() == "on"
|
||||||
|
|||||||
100
scripts/fill-missing-translations.mjs
Executable file
100
scripts/fill-missing-translations.mjs
Executable file
@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
|
const filePath = process.argv[2] ?? 'TheNoiseClock/Localizable.xcstrings';
|
||||||
|
const dryRun = process.argv.includes('--dry-run');
|
||||||
|
|
||||||
|
const raw = await fs.readFile(filePath, 'utf8');
|
||||||
|
const catalog = JSON.parse(raw);
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
async function translate(text, targetLang) {
|
||||||
|
const key = `${targetLang}::${text}`;
|
||||||
|
if (cache.has(key)) return cache.get(key);
|
||||||
|
|
||||||
|
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${encodeURIComponent(targetLang)}&dt=t&q=${encodeURIComponent(text)}`;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0',
|
||||||
|
'Accept': 'application/json, text/plain, */*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
const translated = Array.isArray(body?.[0])
|
||||||
|
? body[0].map((part) => part?.[0] ?? '').join('')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (!translated || translated.trim().length === 0) {
|
||||||
|
throw new Error('Empty translation payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.set(key, translated);
|
||||||
|
await sleep(80);
|
||||||
|
return translated;
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === 3) throw error;
|
||||||
|
await sleep(300 * attempt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalUpdated = 0;
|
||||||
|
let esUpdated = 0;
|
||||||
|
let frUpdated = 0;
|
||||||
|
|
||||||
|
for (const [k, entry] of Object.entries(catalog.strings ?? {})) {
|
||||||
|
const localizations = entry.localizations ?? {};
|
||||||
|
const en = localizations.en?.stringUnit?.value ?? '';
|
||||||
|
if (!en) continue;
|
||||||
|
|
||||||
|
const es = localizations['es-MX']?.stringUnit?.value ?? '';
|
||||||
|
const fr = localizations['fr-CA']?.stringUnit?.value ?? '';
|
||||||
|
|
||||||
|
let touched = false;
|
||||||
|
|
||||||
|
if (!es || es === en) {
|
||||||
|
const translated = await translate(en, 'es');
|
||||||
|
entry.localizations ??= {};
|
||||||
|
entry.localizations['es-MX'] ??= { stringUnit: { state: 'translated', value: '' } };
|
||||||
|
entry.localizations['es-MX'].stringUnit ??= { state: 'translated', value: '' };
|
||||||
|
entry.localizations['es-MX'].stringUnit.state = 'translated';
|
||||||
|
entry.localizations['es-MX'].stringUnit.value = translated;
|
||||||
|
esUpdated += 1;
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fr || fr === en) {
|
||||||
|
const translated = await translate(en, 'fr');
|
||||||
|
entry.localizations ??= {};
|
||||||
|
entry.localizations['fr-CA'] ??= { stringUnit: { state: 'translated', value: '' } };
|
||||||
|
entry.localizations['fr-CA'].stringUnit ??= { state: 'translated', value: '' };
|
||||||
|
entry.localizations['fr-CA'].stringUnit.state = 'translated';
|
||||||
|
entry.localizations['fr-CA'].stringUnit.value = translated;
|
||||||
|
frUpdated += 1;
|
||||||
|
touched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touched) totalUpdated += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
await fs.writeFile(filePath, `${JSON.stringify(catalog, null, 2)}\n`, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
filePath,
|
||||||
|
totalKeysTouched: totalUpdated,
|
||||||
|
esUpdated,
|
||||||
|
frUpdated,
|
||||||
|
dryRun
|
||||||
|
}, null, 2));
|
||||||
Loading…
Reference in New Issue
Block a user