Add Shortcuts intents, entities & localizations

Introduce App Shortcuts and AppIntents support: add multiple shortcut intents (next/closest lesson, today/tomorrow/closest timetable, recent grades, subject/overall averages), AppShortcuts provider, and ShortcutError. Add AppEntity types (Average, Grade, Lesson, Subject) and corresponding entity queries. Refactor Controls to use AppIntent-based navigation (OpenHome/OpenGrades/OpenTimetable) with localized labels/descriptions and remove duplicate SubjectEntity from AveragesIntent. Update TimetableProvider to use a lock-screen-friendly timeline refresh policy. Add localization resources (AppShortcuts.strings and expanded Localizable.strings) for en/de/hu and register new strings in project files; update Info.plist/project.pbxproj accordingly.
This commit is contained in:
Horváth Gergely
2026-01-31 11:52:01 +01:00
committed by 4831c0
parent 71ef509021
commit 874f5d4297
28 changed files with 929 additions and 87 deletions

View File

@@ -4,26 +4,12 @@ import AppIntents
private let appGroup = "group.app.firka.firkaa"
// MARK: - Home Control
// MARK: - Navigation Intents (iOS 16+, used by Controls and Shortcuts)
@available(iOS 18.0, *)
struct HomeControl: ControlWidget {
static let kind = "app.firka.firkaa.control.home"
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: Self.kind) {
ControlWidgetButton(action: OpenHomeIntent()) {
Label("Főoldal", systemImage: "house.fill")
}
}
.displayName("Firka - Főoldal")
.description("Firka app főoldal megnyitása")
}
}
@available(iOS 18.0, *)
@available(iOS 16.0, *)
struct OpenHomeIntent: AppIntent {
static var title: LocalizedStringResource = "Firka Főoldal"
static var title: LocalizedStringResource = LocalizedStringResource("control_home_title", defaultValue: "Firka Home")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("control_home_description", defaultValue: "Open Firka home screen"))
static var openAppWhenRun: Bool = true
func perform() async throws -> some IntentResult {
@@ -32,26 +18,10 @@ struct OpenHomeIntent: AppIntent {
}
}
// MARK: - Grades Control
@available(iOS 18.0, *)
struct GradesControl: ControlWidget {
static let kind = "app.firka.firkaa.control.grades"
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: Self.kind) {
ControlWidgetButton(action: OpenGradesIntent()) {
Label("Jegyek", systemImage: "star.fill")
}
}
.displayName("Firka - Jegyek")
.description("Firka app jegyek megnyitása")
}
}
@available(iOS 18.0, *)
@available(iOS 16.0, *)
struct OpenGradesIntent: AppIntent {
static var title: LocalizedStringResource = "Firka Jegyek"
static var title: LocalizedStringResource = LocalizedStringResource("control_grades_title", defaultValue: "Firka Grades")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("control_grades_description", defaultValue: "Open Firka grades"))
static var openAppWhenRun: Bool = true
func perform() async throws -> some IntentResult {
@@ -60,7 +30,53 @@ struct OpenGradesIntent: AppIntent {
}
}
// MARK: - Timetable Control
@available(iOS 16.0, *)
struct OpenTimetableIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("control_timetable_title", defaultValue: "Firka Timetable")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("control_timetable_description", defaultValue: "Open Firka timetable"))
static var openAppWhenRun: Bool = true
func perform() async throws -> some IntentResult {
UserDefaults(suiteName: appGroup)?.set("timetable", forKey: "controlNavigation")
return .result()
}
}
// MARK: - Home Control (iOS 18+)
@available(iOS 18.0, *)
struct HomeControl: ControlWidget {
static let kind = "app.firka.firkaa.control.home"
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: Self.kind) {
ControlWidgetButton(action: OpenHomeIntent()) {
Label(LocalizedStringResource("control_home_label", defaultValue: "Home"), systemImage: "house.fill")
}
}
.displayName(LocalizedStringResource("control_home_display", defaultValue: "Firka - Home"))
.description(LocalizedStringResource("control_home_description", defaultValue: "Open Firka home screen"))
}
}
// MARK: - Grades Control (iOS 18+)
@available(iOS 18.0, *)
struct GradesControl: ControlWidget {
static let kind = "app.firka.firkaa.control.grades"
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: Self.kind) {
ControlWidgetButton(action: OpenGradesIntent()) {
Label(LocalizedStringResource("control_grades_label", defaultValue: "Grades"), systemImage: "star.fill")
}
}
.displayName(LocalizedStringResource("control_grades_display", defaultValue: "Firka - Grades"))
.description(LocalizedStringResource("control_grades_description", defaultValue: "Open Firka grades"))
}
}
// MARK: - Timetable Control (iOS 18+)
@available(iOS 18.0, *)
struct TimetableControl: ControlWidget {
@@ -69,21 +85,10 @@ struct TimetableControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: Self.kind) {
ControlWidgetButton(action: OpenTimetableIntent()) {
Label("Órarend", systemImage: "calendar")
Label(LocalizedStringResource("control_timetable_label", defaultValue: "Timetable"), systemImage: "calendar")
}
}
.displayName("Firka - Órarend")
.description("Firka app órarend megnyitása")
}
}
@available(iOS 18.0, *)
struct OpenTimetableIntent: AppIntent {
static var title: LocalizedStringResource = "Firka Órarend"
static var openAppWhenRun: Bool = true
func perform() async throws -> some IntentResult {
UserDefaults(suiteName: appGroup)?.set("timetable", forKey: "controlNavigation")
return .result()
.displayName(LocalizedStringResource("control_timetable_display", defaultValue: "Firka - Timetable"))
.description(LocalizedStringResource("control_timetable_description", defaultValue: "Open Firka timetable"))
}
}

View File

@@ -1,40 +1,6 @@
import AppIntents
import WidgetKit
struct SubjectEntity: AppEntity {
let id: String
let name: String
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: LocalizedStringResource("subjects_type", defaultValue: "Subjects"))
}
static var defaultQuery = SubjectQuery()
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
}
struct SubjectQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [SubjectEntity] {
let data = WidgetData.load()
return data?.averages.subjects
.filter { identifiers.contains($0.uid) }
.map { SubjectEntity(id: $0.uid, name: $0.name) } ?? []
}
func suggestedEntities() async throws -> [SubjectEntity] {
let data = WidgetData.load()
return data?.averages.subjects
.map { SubjectEntity(id: $0.uid, name: $0.name) } ?? []
}
func defaultResult() async -> SubjectEntity? {
try? await suggestedEntities().first
}
}
struct AveragesWidgetIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = LocalizedStringResource("widget_averages_title", defaultValue: "Averages")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("widget_averages_description", defaultValue: "Shows subject averages"))

View File

@@ -142,6 +142,18 @@ struct TimetableProvider: AppIntentTimelineProvider {
}
entries.sort { $0.date < $1.date }
if isLockScreenWidget {
var refreshDate: Date
if let next = nextLesson {
refreshDate = next.start
} else if let current = currentLesson {
refreshDate = current.end.addingTimeInterval(1)
} else {
refreshDate = midnight
}
return Timeline(entries: entries, policy: .after(refreshDate))
}
return Timeline(entries: entries, policy: .atEnd)
}

View File

@@ -0,0 +1,55 @@
import AppIntents
import Foundation
@available(iOS 16.0, *)
struct AverageEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: LocalizedStringResource("entity_average", defaultValue: "Average"))
static var defaultQuery = AverageEntityQuery()
var id: String
@Property(title: LocalizedStringResource("entity_prop_subject", defaultValue: "Subject"))
var subjectName: String
@Property(title: LocalizedStringResource("entity_prop_average", defaultValue: "Average"))
var average: Double
@Property(title: LocalizedStringResource("entity_prop_grade_count", defaultValue: "Grade count"))
var gradeCount: Int
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(subjectName)",
subtitle: "\(String(format: "%.2f", average)) (\(gradeCount))"
)
}
init(from avg: SubjectAverage) {
self.id = avg.uid
self.subjectName = avg.name
self.average = avg.average
self.gradeCount = avg.gradeCount
}
init(id: String, subjectName: String, average: Double, gradeCount: Int) {
self.id = id
self.subjectName = subjectName
self.average = average
self.gradeCount = gradeCount
}
}
@available(iOS 16.0, *)
struct AverageEntityQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [AverageEntity] {
guard let data = WidgetData.load() else { return [] }
return data.averages.subjects
.filter { identifiers.contains($0.uid) }
.map { AverageEntity(from: $0) }
}
func suggestedEntities() async throws -> [AverageEntity] {
guard let data = WidgetData.load() else { return [] }
return data.averages.subjects.map { AverageEntity(from: $0) }
}
}

View File

@@ -0,0 +1,80 @@
import AppIntents
import Foundation
@available(iOS 16.0, *)
struct GradeEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: LocalizedStringResource("entity_grade", defaultValue: "Grade"))
static var defaultQuery = GradeEntityQuery()
var id: String
@Property(title: LocalizedStringResource("entity_prop_subject", defaultValue: "Subject"))
var subject: String
@Property(title: LocalizedStringResource("entity_prop_value", defaultValue: "Value"))
var numericValue: Int
@Property(title: LocalizedStringResource("entity_prop_grade", defaultValue: "Grade"))
var strValue: String
@Property(title: LocalizedStringResource("entity_prop_topic", defaultValue: "Topic"))
var topic: String
@Property(title: LocalizedStringResource("entity_prop_type", defaultValue: "Type"))
var type: String
@Property(title: LocalizedStringResource("entity_prop_teacher", defaultValue: "Teacher"))
var teacher: String
@Property(title: LocalizedStringResource("entity_prop_date", defaultValue: "Date"))
var date: Date
@Property(title: LocalizedStringResource("entity_prop_weight", defaultValue: "Weight"))
var weight: Int
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(subject): \(strValue)",
subtitle: "\(topic)"
)
}
init(from grade: WidgetGrade) {
self.id = grade.uid
self.subject = grade.subject.name
self.numericValue = grade.numericValue ?? 0
self.strValue = grade.displayValue
self.topic = grade.topic ?? ""
self.type = grade.type.name
self.teacher = grade.subject.teacherName ?? ""
self.date = grade.recordDate
self.weight = grade.weightPercentage ?? 100
}
init(id: String, subject: String, numericValue: Int, strValue: String, topic: String, type: String, teacher: String, date: Date, weight: Int) {
self.id = id
self.subject = subject
self.numericValue = numericValue
self.strValue = strValue
self.topic = topic
self.type = type
self.teacher = teacher
self.date = date
self.weight = weight
}
}
@available(iOS 16.0, *)
struct GradeEntityQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [GradeEntity] {
guard let data = WidgetData.load() else { return [] }
return data.grades
.filter { identifiers.contains($0.uid) }
.map { GradeEntity(from: $0) }
}
func suggestedEntities() async throws -> [GradeEntity] {
guard let data = WidgetData.load() else { return [] }
return data.grades.prefix(10).map { GradeEntity(from: $0) }
}
}

View File

@@ -0,0 +1,86 @@
import AppIntents
import Foundation
@available(iOS 16.0, *)
struct LessonEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: LocalizedStringResource("entity_lesson", defaultValue: "Lesson"))
static var defaultQuery = LessonEntityQuery()
var id: String
@Property(title: LocalizedStringResource("entity_prop_name", defaultValue: "Name"))
var name: String
@Property(title: LocalizedStringResource("entity_prop_teacher", defaultValue: "Teacher"))
var teacher: String
@Property(title: LocalizedStringResource("entity_prop_room", defaultValue: "Room"))
var room: String
@Property(title: LocalizedStringResource("entity_prop_start_time", defaultValue: "Start time"))
var startTime: Date
@Property(title: LocalizedStringResource("entity_prop_end_time", defaultValue: "End time"))
var endTime: Date
@Property(title: LocalizedStringResource("entity_prop_lesson_number", defaultValue: "Lesson number"))
var lessonNumber: Int
@Property(title: LocalizedStringResource("entity_prop_cancelled", defaultValue: "Cancelled"))
var isCancelled: Bool
@Property(title: LocalizedStringResource("entity_prop_substitution", defaultValue: "Substitution"))
var isSubstitution: Bool
@Property(title: LocalizedStringResource("entity_prop_theme", defaultValue: "Theme"))
var theme: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(lessonNumber). \(room) - \(teacher)"
)
}
init(from lesson: WidgetLesson) {
self.id = lesson.uid
self.name = lesson.subject.name
self.teacher = lesson.teacher ?? ""
self.room = lesson.roomName ?? ""
self.startTime = lesson.start
self.endTime = lesson.end
self.lessonNumber = lesson.lessonNumber ?? 0
self.isCancelled = lesson.isCancelled
self.isSubstitution = lesson.isSubstitution
self.theme = lesson.theme ?? ""
}
init(id: String, name: String, teacher: String, room: String, startTime: Date, endTime: Date, lessonNumber: Int, isCancelled: Bool, isSubstitution: Bool, theme: String) {
self.id = id
self.name = name
self.teacher = teacher
self.room = room
self.startTime = startTime
self.endTime = endTime
self.lessonNumber = lessonNumber
self.isCancelled = isCancelled
self.isSubstitution = isSubstitution
self.theme = theme
}
}
@available(iOS 16.0, *)
struct LessonEntityQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [LessonEntity] {
guard let data = WidgetData.load() else { return [] }
let allLessons = data.timetable.today + data.timetable.tomorrow + (data.timetable.nextSchoolDay ?? [])
return allLessons
.filter { identifiers.contains($0.uid) }
.map { LessonEntity(from: $0) }
}
func suggestedEntities() async throws -> [LessonEntity] {
guard let data = WidgetData.load() else { return [] }
return data.timetable.today.map { LessonEntity(from: $0) }
}
}

View File

@@ -0,0 +1,37 @@
import AppIntents
@available(iOS 16.0, *)
struct SubjectEntity: AppEntity {
let id: String
let name: String
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: LocalizedStringResource("subjects_type", defaultValue: "Subjects"))
}
static var defaultQuery = SubjectQuery()
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
}
@available(iOS 16.0, *)
struct SubjectQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [SubjectEntity] {
let data = WidgetData.load()
return data?.averages.subjects
.filter { identifiers.contains($0.uid) }
.map { SubjectEntity(id: $0.uid, name: $0.name) } ?? []
}
func suggestedEntities() async throws -> [SubjectEntity] {
let data = WidgetData.load()
return data?.averages.subjects
.map { SubjectEntity(id: $0.uid, name: $0.name) } ?? []
}
func defaultResult() async -> SubjectEntity? {
try? await suggestedEntities().first
}
}

View File

@@ -0,0 +1,52 @@
import AppIntents
@available(iOS 16.0, *)
struct FirkaShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: GetNextLessonIntent(),
phrases: [
"Next lesson \(.applicationName)",
"What is my next lesson \(.applicationName)"
],
shortTitle: LocalizedStringResource("shortcut_short_next_lesson", defaultValue: "Next lesson"),
systemImageName: "calendar"
)
AppShortcut(
intent: GetClosestLessonIntent(),
phrases: [
"Closest lesson \(.applicationName)",
"When is my next lesson \(.applicationName)"
],
shortTitle: LocalizedStringResource("shortcut_short_closest_lesson", defaultValue: "Closest lesson"),
systemImageName: "forward"
)
AppShortcut(
intent: GetTodayTimetableIntent(),
phrases: [
"Today's timetable \(.applicationName)",
"What lessons do I have today \(.applicationName)"
],
shortTitle: LocalizedStringResource("shortcut_short_today_timetable", defaultValue: "Today's timetable"),
systemImageName: "clock"
)
AppShortcut(
intent: GetClosestTimetableIntent(),
phrases: [
"Closest timetable \(.applicationName)",
"When are my next lessons \(.applicationName)"
],
shortTitle: LocalizedStringResource("shortcut_short_closest_timetable", defaultValue: "Closest timetable"),
systemImageName: "forward.fill"
)
AppShortcut(
intent: GetOverallAverageIntent(),
phrases: [
"My average \(.applicationName)",
"What is my average \(.applicationName)"
],
shortTitle: LocalizedStringResource("shortcut_short_overall_average", defaultValue: "My average"),
systemImageName: "chart.bar"
)
}
}

View File

@@ -0,0 +1,32 @@
import AppIntents
@available(iOS 16.0, *)
struct GetClosestLessonIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("shortcut_closest_lesson_title", defaultValue: "Closest lesson")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("shortcut_closest_lesson_description", defaultValue: "Get the closest lesson (today, tomorrow, or next school day)"))
func perform() async throws -> some ReturnsValue<LessonEntity> & ProvidesDialog {
guard let data = WidgetData.load() else {
throw ShortcutError.noData
}
let now = Date()
if let lesson = data.timetable.today.first(where: { $0.start > now })
?? data.timetable.today.first(where: { $0.end > now }) {
let entity = LessonEntity(from: lesson)
return .result(value: entity, dialog: "\(entity.name) - \(entity.room)")
}
if let lesson = data.timetable.tomorrow.first {
let entity = LessonEntity(from: lesson)
return .result(value: entity, dialog: "\(entity.name) - \(entity.room)")
}
if let lesson = data.timetable.nextSchoolDay?.first {
let entity = LessonEntity(from: lesson)
return .result(value: entity, dialog: "\(entity.name) - \(entity.room)")
}
throw ShortcutError.noUpcomingLesson
}
}

View File

@@ -0,0 +1,35 @@
import AppIntents
@available(iOS 16.0, *)
struct GetClosestTimetableIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("shortcut_closest_timetable_title", defaultValue: "Closest timetable")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("shortcut_closest_timetable_description", defaultValue: "Get the closest school day's timetable (today, tomorrow, or next school day)"))
func perform() async throws -> some ReturnsValue<[LessonEntity]> & ProvidesDialog {
guard let data = WidgetData.load() else {
throw ShortcutError.noData
}
let now = Date()
let remaining = data.timetable.today.filter { $0.end > now }
if !remaining.isEmpty {
let entities = remaining.map { LessonEntity(from: $0) }
let summary = entities.map { "\($0.lessonNumber). \($0.name)" }.joined(separator: ", ")
return .result(value: entities, dialog: "\(summary)")
}
if !data.timetable.tomorrow.isEmpty {
let entities = data.timetable.tomorrow.map { LessonEntity(from: $0) }
let summary = entities.map { "\($0.lessonNumber). \($0.name)" }.joined(separator: ", ")
return .result(value: entities, dialog: "\(summary)")
}
if let nextDay = data.timetable.nextSchoolDay, !nextDay.isEmpty {
let entities = nextDay.map { LessonEntity(from: $0) }
let summary = entities.map { "\($0.lessonNumber). \($0.name)" }.joined(separator: ", ")
return .result(value: entities, dialog: "\(summary)")
}
throw ShortcutError.noUpcomingLesson
}
}

View File

@@ -0,0 +1,21 @@
import AppIntents
@available(iOS 16.0, *)
struct GetNextLessonIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("shortcut_next_lesson_title", defaultValue: "Next lesson")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("shortcut_next_lesson_description", defaultValue: "Get the next lesson details"))
func perform() async throws -> some ReturnsValue<LessonEntity> & ProvidesDialog {
guard let data = WidgetData.load() else {
throw ShortcutError.noData
}
let now = Date()
let upcoming = data.timetable.today.first { $0.start > now }
?? data.timetable.today.first { $0.end > now }
guard let lesson = upcoming else {
throw ShortcutError.noUpcomingLesson
}
let entity = LessonEntity(from: lesson)
return .result(value: entity, dialog: "\(entity.name) - \(entity.room)")
}
}

View File

@@ -0,0 +1,18 @@
import AppIntents
@available(iOS 16.0, *)
struct GetOverallAverageIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("shortcut_overall_average_title", defaultValue: "Overall average")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("shortcut_overall_average_description", defaultValue: "Get the overall academic average"))
func perform() async throws -> some ReturnsValue<Double> & ProvidesDialog {
guard let data = WidgetData.load() else {
throw ShortcutError.noData
}
guard let overall = data.averages.overall else {
throw ShortcutError.noData
}
let rounded = (overall * 100).rounded() / 100
return .result(value: rounded, dialog: "\(String(format: "%.2f", rounded))")
}
}

View File

@@ -0,0 +1,20 @@
import AppIntents
@available(iOS 16.0, *)
struct GetRecentGradesIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("shortcut_recent_grades_title", defaultValue: "Recent grades")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("shortcut_recent_grades_description", defaultValue: "Get the most recent grades"))
@Parameter(title: LocalizedStringResource("shortcut_param_count", defaultValue: "Count"), default: 5)
var count: Int
func perform() async throws -> some ReturnsValue<[GradeEntity]> & ProvidesDialog {
guard let data = WidgetData.load() else {
throw ShortcutError.noData
}
let grades = Array(data.grades.prefix(count))
let entities = grades.map { GradeEntity(from: $0) }
let summary = entities.map { "\($0.subject): \($0.strValue)" }.joined(separator: ", ")
return .result(value: entities, dialog: "\(summary)")
}
}

View File

@@ -0,0 +1,21 @@
import AppIntents
@available(iOS 16.0, *)
struct GetSubjectAverageIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("shortcut_subject_average_title", defaultValue: "Subject average")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("shortcut_subject_average_description", defaultValue: "Get the average for a subject"))
@Parameter(title: LocalizedStringResource("shortcut_param_subject", defaultValue: "Subject"))
var subject: SubjectEntity
func perform() async throws -> some ReturnsValue<AverageEntity> & ProvidesDialog {
guard let data = WidgetData.load() else {
throw ShortcutError.noData
}
guard let avg = data.averages.subjects.first(where: { $0.uid == subject.id }) else {
throw ShortcutError.subjectNotFound
}
let entity = AverageEntity(from: avg)
return .result(value: entity, dialog: "\(entity.subjectName): \(String(format: "%.2f", entity.average))")
}
}

View File

@@ -0,0 +1,19 @@
import AppIntents
@available(iOS 16.0, *)
struct GetTodayTimetableIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("shortcut_today_timetable_title", defaultValue: "Today's timetable")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("shortcut_today_timetable_description", defaultValue: "Get today's full timetable"))
func perform() async throws -> some ReturnsValue<[LessonEntity]> & ProvidesDialog {
guard let data = WidgetData.load() else {
throw ShortcutError.noData
}
guard !data.timetable.today.isEmpty else {
throw ShortcutError.noLessonsToday
}
let entities = data.timetable.today.map { LessonEntity(from: $0) }
let summary = entities.map { "\($0.lessonNumber). \($0.name)" }.joined(separator: ", ")
return .result(value: entities, dialog: "\(summary)")
}
}

View File

@@ -0,0 +1,19 @@
import AppIntents
@available(iOS 16.0, *)
struct GetTomorrowTimetableIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("shortcut_tomorrow_timetable_title", defaultValue: "Tomorrow's timetable")
static var description: IntentDescription = IntentDescription(LocalizedStringResource("shortcut_tomorrow_timetable_description", defaultValue: "Get tomorrow's timetable"))
func perform() async throws -> some ReturnsValue<[LessonEntity]> & ProvidesDialog {
guard let data = WidgetData.load() else {
throw ShortcutError.noData
}
guard !data.timetable.tomorrow.isEmpty else {
throw ShortcutError.noLessonsTomorrow
}
let entities = data.timetable.tomorrow.map { LessonEntity(from: $0) }
let summary = entities.map { "\($0.lessonNumber). \($0.name)" }.joined(separator: ", ")
return .result(value: entities, dialog: "\(summary)")
}
}

View File

@@ -0,0 +1,25 @@
import Foundation
@available(iOS 16.0, *)
enum ShortcutError: Error, CustomLocalizedStringResourceConvertible {
case noData
case noUpcomingLesson
case noLessonsToday
case noLessonsTomorrow
case subjectNotFound
var localizedStringResource: LocalizedStringResource {
switch self {
case .noData:
return LocalizedStringResource("shortcut_error_no_data", defaultValue: "No data available. Open the Firka app to refresh.")
case .noUpcomingLesson:
return LocalizedStringResource("shortcut_error_no_upcoming_lesson", defaultValue: "No more lessons today.")
case .noLessonsToday:
return LocalizedStringResource("shortcut_error_no_lessons_today", defaultValue: "No lessons today.")
case .noLessonsTomorrow:
return LocalizedStringResource("shortcut_error_no_lessons_tomorrow", defaultValue: "No lessons tomorrow.")
case .subjectNotFound:
return LocalizedStringResource("shortcut_error_subject_not_found", defaultValue: "Subject not found.")
}
}
}

View File

@@ -0,0 +1,11 @@
/* Siri Phrases - German */
"Next lesson ${applicationName}" = "Nächste Stunde ${applicationName}";
"What is my next lesson ${applicationName}" = "Was ist meine nächste Stunde ${applicationName}";
"Closest lesson ${applicationName}" = "Nächste anstehende Stunde ${applicationName}";
"When is my next lesson ${applicationName}" = "Wann ist meine nächste Stunde ${applicationName}";
"Today's timetable ${applicationName}" = "Stundenplan heute ${applicationName}";
"What lessons do I have today ${applicationName}" = "Welche Stunden habe ich heute ${applicationName}";
"Closest timetable ${applicationName}" = "Nächster Stundenplan ${applicationName}";
"When are my next lessons ${applicationName}" = "Wann sind meine nächsten Stunden ${applicationName}";
"My average ${applicationName}" = "Mein Durchschnitt ${applicationName}";
"What is my average ${applicationName}" = "Wie ist mein Durchschnitt ${applicationName}";

View File

@@ -31,3 +31,90 @@
/* Subject Selection */
"subjects_type" = "Fächer";
"no_subjects_available" = "Keine Fächer verfügbar";
/* Control Widget Titles */
"control_home_title" = "Firka Startseite";
"control_home_label" = "Startseite";
"control_home_display" = "Firka - Startseite";
"control_home_description" = "Firka Startseite öffnen";
"control_grades_title" = "Firka Noten";
"control_grades_label" = "Noten";
"control_grades_display" = "Firka - Noten";
"control_grades_description" = "Firka Noten öffnen";
"control_timetable_title" = "Firka Stundenplan";
"control_timetable_label" = "Stundenplan";
"control_timetable_display" = "Firka - Stundenplan";
"control_timetable_description" = "Firka Stundenplan öffnen";
/* Shortcut Intent Titles & Descriptions */
"shortcut_next_lesson_title" = "Nächste Stunde";
"shortcut_next_lesson_description" = "Details zur nächsten Stunde";
"shortcut_closest_lesson_title" = "Nächste anstehende Stunde";
"shortcut_closest_lesson_description" = "Nächste anstehende Stunde (heute, morgen oder nächster Schultag)";
"shortcut_today_timetable_title" = "Stundenplan heute";
"shortcut_today_timetable_description" = "Der vollständige Stundenplan für heute";
"shortcut_tomorrow_timetable_title" = "Stundenplan morgen";
"shortcut_tomorrow_timetable_description" = "Der Stundenplan für morgen";
"shortcut_closest_timetable_title" = "Nächster Stundenplan";
"shortcut_closest_timetable_description" = "Stundenplan des nächsten Schultags (heute, morgen oder nächster Schultag)";
"shortcut_recent_grades_title" = "Letzte Noten";
"shortcut_recent_grades_description" = "Die letzten Noten";
"shortcut_subject_average_title" = "Fachdurchschnitt";
"shortcut_subject_average_description" = "Durchschnitt eines Fachs";
"shortcut_overall_average_title" = "Gesamtdurchschnitt";
"shortcut_overall_average_description" = "Der Gesamtdurchschnitt";
/* Shortcut Parameters */
"shortcut_param_count" = "Anzahl";
"shortcut_param_subject" = "Fach";
/* Shortcut Phrases */
"shortcut_phrase_next_lesson" = "Nächste Stunde";
"shortcut_phrase_next_lesson_question" = "Was ist meine nächste Stunde";
"shortcut_phrase_closest_lesson" = "Nächste anstehende Stunde";
"shortcut_phrase_closest_lesson_question" = "Wann ist meine nächste Stunde";
"shortcut_phrase_today_timetable" = "Stundenplan heute";
"shortcut_phrase_today_timetable_question" = "Welche Stunden habe ich heute";
"shortcut_phrase_closest_timetable" = "Nächster Stundenplan";
"shortcut_phrase_closest_timetable_question" = "Wann sind meine nächsten Stunden";
"shortcut_phrase_overall_average" = "Mein Durchschnitt";
"shortcut_phrase_overall_average_question" = "Wie ist mein Durchschnitt";
/* Shortcut Short Titles */
"shortcut_short_next_lesson" = "Nächste Stunde";
"shortcut_short_closest_lesson" = "Nächste anstehende";
"shortcut_short_today_timetable" = "Stundenplan heute";
"shortcut_short_closest_timetable" = "Nächster Stundenplan";
"shortcut_short_overall_average" = "Mein Durchschnitt";
/* Shortcut Errors */
"shortcut_error_no_data" = "Keine Daten verfügbar. Öffne die Firka App zum Aktualisieren.";
"shortcut_error_no_upcoming_lesson" = "Heute keine weiteren Stunden.";
"shortcut_error_no_lessons_today" = "Heute keine Stunden.";
"shortcut_error_no_lessons_tomorrow" = "Morgen keine Stunden.";
"shortcut_error_subject_not_found" = "Fach nicht gefunden.";
/* Entity Type Names */
"entity_lesson" = "Stunde";
"entity_grade" = "Note";
"entity_average" = "Durchschnitt";
/* Entity Property Names */
"entity_prop_name" = "Name";
"entity_prop_teacher" = "Lehrer";
"entity_prop_room" = "Raum";
"entity_prop_start_time" = "Startzeit";
"entity_prop_end_time" = "Endzeit";
"entity_prop_lesson_number" = "Stundennummer";
"entity_prop_cancelled" = "Entfällt";
"entity_prop_substitution" = "Vertretung";
"entity_prop_theme" = "Thema";
"entity_prop_subject" = "Fach";
"entity_prop_value" = "Wert";
"entity_prop_grade" = "Note";
"entity_prop_topic" = "Thema";
"entity_prop_type" = "Typ";
"entity_prop_date" = "Datum";
"entity_prop_weight" = "Gewicht";
"entity_prop_average" = "Durchschnitt";
"entity_prop_grade_count" = "Notenanzahl";

View File

@@ -0,0 +1,11 @@
/* Siri Phrases - English */
"Next lesson ${applicationName}" = "Next lesson ${applicationName}";
"What is my next lesson ${applicationName}" = "What is my next lesson ${applicationName}";
"Closest lesson ${applicationName}" = "Closest lesson ${applicationName}";
"When is my next lesson ${applicationName}" = "When is my next lesson ${applicationName}";
"Today's timetable ${applicationName}" = "Today's timetable ${applicationName}";
"What lessons do I have today ${applicationName}" = "What lessons do I have today ${applicationName}";
"Closest timetable ${applicationName}" = "Closest timetable ${applicationName}";
"When are my next lessons ${applicationName}" = "When are my next lessons ${applicationName}";
"My average ${applicationName}" = "My average ${applicationName}";
"What is my average ${applicationName}" = "What is my average ${applicationName}";

View File

@@ -31,3 +31,90 @@
/* Subject Selection */
"subjects_type" = "Subjects";
"no_subjects_available" = "No subjects available";
/* Control Widget Titles */
"control_home_title" = "Firka Home";
"control_home_label" = "Home";
"control_home_display" = "Firka - Home";
"control_home_description" = "Open Firka home screen";
"control_grades_title" = "Firka Grades";
"control_grades_label" = "Grades";
"control_grades_display" = "Firka - Grades";
"control_grades_description" = "Open Firka grades";
"control_timetable_title" = "Firka Timetable";
"control_timetable_label" = "Timetable";
"control_timetable_display" = "Firka - Timetable";
"control_timetable_description" = "Open Firka timetable";
/* Shortcut Intent Titles & Descriptions */
"shortcut_next_lesson_title" = "Next lesson";
"shortcut_next_lesson_description" = "Get the next lesson details";
"shortcut_closest_lesson_title" = "Closest lesson";
"shortcut_closest_lesson_description" = "Get the closest lesson (today, tomorrow, or next school day)";
"shortcut_today_timetable_title" = "Today's timetable";
"shortcut_today_timetable_description" = "Get today's full timetable";
"shortcut_tomorrow_timetable_title" = "Tomorrow's timetable";
"shortcut_tomorrow_timetable_description" = "Get tomorrow's timetable";
"shortcut_closest_timetable_title" = "Closest timetable";
"shortcut_closest_timetable_description" = "Get the closest school day's timetable (today, tomorrow, or next school day)";
"shortcut_recent_grades_title" = "Recent grades";
"shortcut_recent_grades_description" = "Get the most recent grades";
"shortcut_subject_average_title" = "Subject average";
"shortcut_subject_average_description" = "Get the average for a subject";
"shortcut_overall_average_title" = "Overall average";
"shortcut_overall_average_description" = "Get the overall academic average";
/* Shortcut Parameters */
"shortcut_param_count" = "Count";
"shortcut_param_subject" = "Subject";
/* Shortcut Phrases */
"shortcut_phrase_next_lesson" = "Next lesson";
"shortcut_phrase_next_lesson_question" = "What is my next lesson";
"shortcut_phrase_closest_lesson" = "Closest lesson";
"shortcut_phrase_closest_lesson_question" = "When is my next lesson";
"shortcut_phrase_today_timetable" = "Today's timetable";
"shortcut_phrase_today_timetable_question" = "What lessons do I have today";
"shortcut_phrase_closest_timetable" = "Closest timetable";
"shortcut_phrase_closest_timetable_question" = "When are my next lessons";
"shortcut_phrase_overall_average" = "My average";
"shortcut_phrase_overall_average_question" = "What is my average";
/* Shortcut Short Titles */
"shortcut_short_next_lesson" = "Next lesson";
"shortcut_short_closest_lesson" = "Closest lesson";
"shortcut_short_today_timetable" = "Today's timetable";
"shortcut_short_closest_timetable" = "Closest timetable";
"shortcut_short_overall_average" = "My average";
/* Shortcut Errors */
"shortcut_error_no_data" = "No data available. Open the Firka app to refresh.";
"shortcut_error_no_upcoming_lesson" = "No more lessons today.";
"shortcut_error_no_lessons_today" = "No lessons today.";
"shortcut_error_no_lessons_tomorrow" = "No lessons tomorrow.";
"shortcut_error_subject_not_found" = "Subject not found.";
/* Entity Type Names */
"entity_lesson" = "Lesson";
"entity_grade" = "Grade";
"entity_average" = "Average";
/* Entity Property Names */
"entity_prop_name" = "Name";
"entity_prop_teacher" = "Teacher";
"entity_prop_room" = "Room";
"entity_prop_start_time" = "Start time";
"entity_prop_end_time" = "End time";
"entity_prop_lesson_number" = "Lesson number";
"entity_prop_cancelled" = "Cancelled";
"entity_prop_substitution" = "Substitution";
"entity_prop_theme" = "Theme";
"entity_prop_subject" = "Subject";
"entity_prop_value" = "Value";
"entity_prop_grade" = "Grade";
"entity_prop_topic" = "Topic";
"entity_prop_type" = "Type";
"entity_prop_date" = "Date";
"entity_prop_weight" = "Weight";
"entity_prop_average" = "Average";
"entity_prop_grade_count" = "Grade count";

View File

@@ -0,0 +1,11 @@
/* Siri Phrases - Hungarian */
"Next lesson ${applicationName}" = "Következő óra ${applicationName}";
"What is my next lesson ${applicationName}" = "Mi a következő órám ${applicationName}";
"Closest lesson ${applicationName}" = "Legközelebbi óra ${applicationName}";
"When is my next lesson ${applicationName}" = "Mikor lesz órám ${applicationName}";
"Today's timetable ${applicationName}" = "Mai órarend ${applicationName}";
"What lessons do I have today ${applicationName}" = "Milyen óráim vannak ma ${applicationName}";
"Closest timetable ${applicationName}" = "Legközelebbi órarend ${applicationName}";
"When are my next lessons ${applicationName}" = "Mikor lesznek óráim ${applicationName}";
"My average ${applicationName}" = "Átlagom ${applicationName}";
"What is my average ${applicationName}" = "Mennyi az átlagom ${applicationName}";

View File

@@ -31,3 +31,90 @@
/* Subject Selection */
"subjects_type" = "Tantárgyak";
"no_subjects_available" = "Nincsenek tantárgyak";
/* Control Widget Titles */
"control_home_title" = "Firka Főoldal";
"control_home_label" = "Főoldal";
"control_home_display" = "Firka - Főoldal";
"control_home_description" = "Firka app főoldal megnyitása";
"control_grades_title" = "Firka Jegyek";
"control_grades_label" = "Jegyek";
"control_grades_display" = "Firka - Jegyek";
"control_grades_description" = "Firka app jegyek megnyitása";
"control_timetable_title" = "Firka Órarend";
"control_timetable_label" = "Órarend";
"control_timetable_display" = "Firka - Órarend";
"control_timetable_description" = "Firka app órarend megnyitása";
/* Shortcut Intent Titles & Descriptions */
"shortcut_next_lesson_title" = "Következő óra";
"shortcut_next_lesson_description" = "A következő óra adatai";
"shortcut_closest_lesson_title" = "Legközelebbi óra";
"shortcut_closest_lesson_description" = "A legközelebbi óra adatai (ma, holnap, vagy a következő tanítási nap)";
"shortcut_today_timetable_title" = "Mai órarend";
"shortcut_today_timetable_description" = "A mai nap teljes órarendje";
"shortcut_tomorrow_timetable_title" = "Holnapi órarend";
"shortcut_tomorrow_timetable_description" = "A holnapi nap órarendje";
"shortcut_closest_timetable_title" = "Legközelebbi órarend";
"shortcut_closest_timetable_description" = "A legközelebbi tanítási nap órarendje (ma, holnap, vagy a következő tanítási nap)";
"shortcut_recent_grades_title" = "Legutóbbi jegyek";
"shortcut_recent_grades_description" = "A legutóbbi jegyek listája";
"shortcut_subject_average_title" = "Tantárgyi átlag";
"shortcut_subject_average_description" = "Egy tantárgy átlaga";
"shortcut_overall_average_title" = "Összesített átlag";
"shortcut_overall_average_description" = "Az összesített tanulmányi átlag";
/* Shortcut Parameters */
"shortcut_param_count" = "Darabszám";
"shortcut_param_subject" = "Tantárgy";
/* Shortcut Phrases */
"shortcut_phrase_next_lesson" = "Következő óra";
"shortcut_phrase_next_lesson_question" = "Mi a következő órám";
"shortcut_phrase_closest_lesson" = "Legközelebbi óra";
"shortcut_phrase_closest_lesson_question" = "Mikor lesz órám";
"shortcut_phrase_today_timetable" = "Mai órarend";
"shortcut_phrase_today_timetable_question" = "Milyen óráim vannak ma";
"shortcut_phrase_closest_timetable" = "Legközelebbi órarend";
"shortcut_phrase_closest_timetable_question" = "Mikor lesznek óráim";
"shortcut_phrase_overall_average" = "Átlagom";
"shortcut_phrase_overall_average_question" = "Mennyi az átlagom";
/* Shortcut Short Titles */
"shortcut_short_next_lesson" = "Következő óra";
"shortcut_short_closest_lesson" = "Legközelebbi óra";
"shortcut_short_today_timetable" = "Mai órarend";
"shortcut_short_closest_timetable" = "Legközelebbi órarend";
"shortcut_short_overall_average" = "Átlagom";
/* Shortcut Errors */
"shortcut_error_no_data" = "Nincs elérhető adat. Nyisd meg a Firka appot az adatok frissítéséhez.";
"shortcut_error_no_upcoming_lesson" = "Ma nincs több órád.";
"shortcut_error_no_lessons_today" = "Ma nincs órád.";
"shortcut_error_no_lessons_tomorrow" = "Holnap nincs órád.";
"shortcut_error_subject_not_found" = "A tantárgy nem található.";
/* Entity Type Names */
"entity_lesson" = "Óra";
"entity_grade" = "Jegy";
"entity_average" = "Átlag";
/* Entity Property Names */
"entity_prop_name" = "Név";
"entity_prop_teacher" = "Tanár";
"entity_prop_room" = "Terem";
"entity_prop_start_time" = "Kezdés";
"entity_prop_end_time" = "Vége";
"entity_prop_lesson_number" = "Óraszám";
"entity_prop_cancelled" = "Elmarad";
"entity_prop_substitution" = "Helyettesítés";
"entity_prop_theme" = "Téma";
"entity_prop_subject" = "Tantárgy";
"entity_prop_value" = "Érték";
"entity_prop_grade" = "Jegy";
"entity_prop_topic" = "Téma";
"entity_prop_type" = "Típus";
"entity_prop_date" = "Dátum";
"entity_prop_weight" = "Súly";
"entity_prop_average" = "Átlag";
"entity_prop_grade_count" = "Jegyek száma";

View File

@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
AA00000100000005AABBCC05 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = AA00000100000004AABBCC04 /* Localizable.strings */; };
14578EED4EA309B337AB389E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A749415A687CBFC3F46FA876 /* Pods_RunnerTests.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
213F8C0F6B5418B02DE14204 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 035E9CCBCC6585D0F5639031 /* Pods_Runner.framework */; };
@@ -109,6 +110,9 @@
AB2E15171B6907C52E8C2B42 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
AE756C46C544099A30412EAF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
EBD040A65B2746AF6A3D5C40 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
AA00000100000001AABBCC01 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
AA00000100000002AABBCC02 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
AA00000100000003AABBCC03 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -284,6 +288,7 @@
isa = PBXGroup;
children = (
4F25FCBD2EB1790E0060DAAA /* Runner.entitlements */,
AA00000100000004AABBCC04 /* Localizable.strings */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -462,6 +467,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
AA00000100000005AABBCC05 /* Localizable.strings in Resources */,
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
@@ -654,6 +660,16 @@
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
AA00000100000004AABBCC04 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
AA00000100000001AABBCC01 /* hu */,
AA00000100000002AABBCC02 /* en */,
AA00000100000003AABBCC03 /* de */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */

View File

@@ -8,8 +8,16 @@
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>hu</string>
<string>de</string>
</array>
<key>CFBundleDisplayName</key>
<string>Firka Testing</string>
<key>CFBundleExecutable</key>

View File

@@ -0,0 +1,7 @@
/* Navigation Intent Titles (shared with HomeWidgetsExtension) */
"control_home_title" = "Firka Startseite";
"control_home_description" = "Firka Startseite öffnen";
"control_grades_title" = "Firka Noten";
"control_grades_description" = "Firka Noten öffnen";
"control_timetable_title" = "Firka Stundenplan";
"control_timetable_description" = "Firka Stundenplan öffnen";

View File

@@ -0,0 +1,7 @@
/* Navigation Intent Titles (shared with HomeWidgetsExtension) */
"control_home_title" = "Firka Home";
"control_home_description" = "Open Firka home screen";
"control_grades_title" = "Firka Grades";
"control_grades_description" = "Open Firka grades";
"control_timetable_title" = "Firka Timetable";
"control_timetable_description" = "Open Firka timetable";

View File

@@ -0,0 +1,7 @@
/* Navigation Intent Titles (shared with HomeWidgetsExtension) */
"control_home_title" = "Firka Főoldal";
"control_home_description" = "Firka app főoldal megnyitása";
"control_grades_title" = "Firka Jegyek";
"control_grades_description" = "Firka app jegyek megnyitása";
"control_timetable_title" = "Firka Órarend";
"control_timetable_description" = "Firka app órarend megnyitása";