better presentation fot the App Store
BIN
Cable/AppIcon copy.icon/Assets/body 3.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
Cable/AppIcon copy.icon/Assets/fuse-top.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
Cable/AppIcon copy.icon/Assets/legs 2.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
295
Cable/AppIcon copy.icon/icon.json
Normal file
@@ -0,0 +1,295 @@
|
||||
{
|
||||
"fill" : {
|
||||
"automatic-gradient" : "extended-gray:1.00000,1.00000"
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "display-p3:0.31812,0.56494,0.59766,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"image-name" : "fuse-top.png",
|
||||
"name" : "fuse-top",
|
||||
"opacity" : 1,
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
-225.390625
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"automatic-gradient" : "display-p3:0.31812,0.56494,0.59766,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.83742,0.83744,0.83743,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "body 3.png",
|
||||
"name" : "body 3",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
-74.9921875
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"name" : "Group",
|
||||
"opacity" : 0.8,
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"hidden" : false,
|
||||
"layers" : [
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.37056,0.37056,0.37056,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.75407,0.75409,0.75408,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.75407,0.75409,0.75408,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : true
|
||||
}
|
||||
],
|
||||
"image-name" : "legs 2.png",
|
||||
"name" : "legs 2",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
100
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "combined",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : false,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,0.57811,0.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"image-name" : "fuse-top.png",
|
||||
"name" : "fuse-top",
|
||||
"opacity" : 1,
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
-225.390625
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:1.00000,0.57811,0.00000,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.83742,0.83744,0.83743,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "body 3.png",
|
||||
"name" : "body 3",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
-74.9921875
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"name" : "Group",
|
||||
"opacity" : 0.8,
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
65.078125,
|
||||
-49.375
|
||||
]
|
||||
},
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"hidden" : false,
|
||||
"layers" : [
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.37056,0.37056,0.37056,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.75407,0.75409,0.75408,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.75407,0.75409,0.75408,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : true
|
||||
}
|
||||
],
|
||||
"image-name" : "legs 2.png",
|
||||
"name" : "legs 2",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
-14.34375,
|
||||
117.640625
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "combined",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
73.96875,
|
||||
-62.640625
|
||||
]
|
||||
},
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : false,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
||||
BIN
Cable/AppIcon.icon/Assets/body 3.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 71 KiB |
BIN
Cable/AppIcon.icon/Assets/flash.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
Cable/AppIcon.icon/Assets/fuse-top.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
Cable/AppIcon.icon/Assets/legs 2.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,63 +1,171 @@
|
||||
{
|
||||
"fill" : {
|
||||
"automatic-gradient" : "display-p3:0.50588,0.79216,0.56471,1.00000"
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.92941,1.00000,0.92941,1.00000",
|
||||
"extended-gray:1.00000,1.00000"
|
||||
]
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"blur-material" : 0.9,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"glass" : false,
|
||||
"hidden" : false,
|
||||
"image-name" : "voltplan-lines.png",
|
||||
"name" : "voltplan-lines",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
80.8265625
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"srgb:1.00000,1.00000,1.00000,1.00000",
|
||||
"srgb:1.00000,1.00000,1.00000,0.63382"
|
||||
]
|
||||
"solid" : "srgb:1.00000,0.57811,0.00000,1.00000"
|
||||
},
|
||||
"image-name" : "voltplan-logo 2 2.png",
|
||||
"name" : "voltplan-logo 2 2",
|
||||
"glass" : true,
|
||||
"hidden" : true,
|
||||
"image-name" : "flash.png",
|
||||
"name" : "flash",
|
||||
"opacity" : 1,
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"scale" : 0.8,
|
||||
"translation-in-points" : [
|
||||
-3,
|
||||
77.55625
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"srgb:1.00000,1.00000,1.00000,1.00000",
|
||||
"srgb:1.00000,1.00000,1.00000,0.50000"
|
||||
]
|
||||
},
|
||||
"image-name" : "box-2.png",
|
||||
"name" : "Layer",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
-91.0703125
|
||||
7.4921875,
|
||||
-218.84375
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : false,
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"solid" : "display-p3:0.31812,0.56494,0.59766,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"solid" : "srgb:1.00000,1.00000,1.00000,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"image-name" : "fuse-top.png",
|
||||
"name" : "fuse-top",
|
||||
"opacity" : 1,
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
-225.390625
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"automatic-gradient" : "display-p3:0.31812,0.56494,0.59766,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.83742,0.83744,0.83743,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "body 3.png",
|
||||
"name" : "body 3",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"value" : 1
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
-74.9921875
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"name" : "Group",
|
||||
"opacity" : 0.8,
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"hidden" : false,
|
||||
"layers" : [
|
||||
{
|
||||
"fill-specializations" : [
|
||||
{
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.37056,0.37056,0.37056,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "dark",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.75407,0.75409,0.75408,1.00000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : {
|
||||
"automatic-gradient" : "srgb:0.75407,0.75409,0.75408,1.00000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"glass-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : true
|
||||
}
|
||||
],
|
||||
"image-name" : "legs 2.png",
|
||||
"name" : "legs 2",
|
||||
"opacity-specializations" : [
|
||||
{
|
||||
"appearance" : "tinted",
|
||||
"value" : 1
|
||||
}
|
||||
],
|
||||
"position" : {
|
||||
"scale" : 0.9,
|
||||
"translation-in-points" : [
|
||||
0,
|
||||
100
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "combined",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : false,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
|
||||
@@ -43,3 +43,27 @@
|
||||
"sample.load.lighting.name" = "LED strip lighting";
|
||||
"sample.load.compressor.name" = "Air compressor";
|
||||
"sample.load.charger.name" = "Tool charger";
|
||||
"system.icon.keywords.rv" = "rv, van, camper, motorhome, coach";
|
||||
"system.icon.keywords.truck" = "truck, trailer, rig";
|
||||
"system.icon.keywords.boat" = "boat, marine, yacht, sail";
|
||||
"system.icon.keywords.plane" = "plane, air, flight";
|
||||
"system.icon.keywords.ferry" = "ferry, ship";
|
||||
"system.icon.keywords.house" = "house, home, cabin, cottage, lodge";
|
||||
"system.icon.keywords.building" = "building, office, warehouse, factory, facility";
|
||||
"system.icon.keywords.tent" = "camp, tent, outdoor";
|
||||
"system.icon.keywords.solar" = "solar, sun";
|
||||
"system.icon.keywords.battery" = "battery, storage";
|
||||
"system.icon.keywords.server" = "server, data, network, rack";
|
||||
"system.icon.keywords.computer" = "computer, electronics, lab, tech";
|
||||
"system.icon.keywords.gear" = "gear, mechanic, machine, workshop";
|
||||
"system.icon.keywords.tool" = "tool, maintenance, repair, shop";
|
||||
"system.icon.keywords.hammer" = "hammer, carpentry";
|
||||
"system.icon.keywords.light" = "light, lighting, lamp";
|
||||
"system.icon.keywords.bolt" = "bolt, power, electric";
|
||||
"system.icon.keywords.plug" = "plug";
|
||||
"system.icon.keywords.engine" = "engine, generator, motor";
|
||||
"system.icon.keywords.fuel" = "fuel, diesel, gas";
|
||||
"system.icon.keywords.water" = "water, pump, tank";
|
||||
"system.icon.keywords.heat" = "heat, heater, furnace";
|
||||
"system.icon.keywords.cold" = "cold, freeze, cool";
|
||||
"system.icon.keywords.climate" = "climate, hvac, temperature";
|
||||
|
||||
@@ -9,6 +9,7 @@ struct ComponentLibraryItem: Identifiable, Equatable {
|
||||
|
||||
let id: String
|
||||
let name: String
|
||||
let translations: [String: String]
|
||||
let voltageIn: Double?
|
||||
let voltageOut: Double?
|
||||
let watt: Double?
|
||||
@@ -39,10 +40,24 @@ struct ComponentLibraryItem: Identifiable, Equatable {
|
||||
return String(format: "%.1fA", current)
|
||||
}
|
||||
|
||||
var localizedName: String {
|
||||
localizedName(usingPreferredLanguages: Locale.preferredLanguages) ?? name
|
||||
}
|
||||
|
||||
func localizedName(usingPreferredLanguages languages: [String]) -> String? {
|
||||
guard let primaryIdentifier = languages.first else { return nil }
|
||||
let locale = Locale(identifier: primaryIdentifier)
|
||||
return translation(for: locale)
|
||||
}
|
||||
|
||||
var primaryAffiliateLink: AffiliateLink? {
|
||||
affiliateLink(matching: Locale.current.region?.identifier)
|
||||
}
|
||||
|
||||
func localizedName(for locale: Locale) -> String {
|
||||
translation(for: locale) ?? name
|
||||
}
|
||||
|
||||
func affiliateLink(matching regionCode: String?) -> AffiliateLink? {
|
||||
guard !affiliateLinks.isEmpty else { return nil }
|
||||
|
||||
@@ -64,6 +79,108 @@ struct ComponentLibraryItem: Identifiable, Equatable {
|
||||
|
||||
return affiliateLinks.first
|
||||
}
|
||||
|
||||
private func translation(for locale: Locale) -> String? {
|
||||
guard !translations.isEmpty else { return nil }
|
||||
|
||||
let lookupKeys = ComponentLibraryItem.lookupKeys(for: locale)
|
||||
|
||||
for key in lookupKeys {
|
||||
if let match = translations[key] {
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
let normalizedTranslations = translations.reduce(into: [String: String]()) { result, element in
|
||||
let normalizedKey = ComponentLibraryItem.normalizeLocaleKey(element.key)
|
||||
result[normalizedKey] = element.value
|
||||
|
||||
if let languageOnlyKey = ComponentLibraryItem.languageComponent(fromNormalizedKey: normalizedKey),
|
||||
result[languageOnlyKey] == nil {
|
||||
result[languageOnlyKey] = element.value
|
||||
}
|
||||
}
|
||||
|
||||
for key in lookupKeys {
|
||||
let normalizedKey = ComponentLibraryItem.normalizeLocaleKey(key)
|
||||
if let match = normalizedTranslations[normalizedKey] {
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func lookupKeys(for locale: Locale) -> [String] {
|
||||
var keys: [String] = []
|
||||
|
||||
func append(_ value: String?) {
|
||||
guard let value, !value.isEmpty else { return }
|
||||
|
||||
for variant in variants(for: value) {
|
||||
if !keys.contains(variant) {
|
||||
keys.append(variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
append(locale.identifier)
|
||||
|
||||
let components = Locale.components(fromIdentifier: locale.identifier)
|
||||
|
||||
if let language = components[NSLocale.Key.languageCode.rawValue]?.lowercased() {
|
||||
if let region = components[NSLocale.Key.countryCode.rawValue]?.uppercased() {
|
||||
append("\(language)_\(region)")
|
||||
}
|
||||
append(language)
|
||||
}
|
||||
|
||||
if let languageCode = locale.language.languageCode?.identifier.lowercased() {
|
||||
append(languageCode)
|
||||
}
|
||||
|
||||
if let regionIdentifier = locale.region?.identifier.uppercased(),
|
||||
let languageIdentifier = locale.language.languageCode?.identifier.lowercased() {
|
||||
append("\(languageIdentifier)_\(regionIdentifier)")
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
private static func normalizeLocaleKey(_ key: String) -> String {
|
||||
let sanitized = key.replacingOccurrences(of: "-", with: "_")
|
||||
let parts = sanitized.split(separator: "_", omittingEmptySubsequences: true)
|
||||
|
||||
guard let languagePart = parts.first else {
|
||||
return sanitized.lowercased()
|
||||
}
|
||||
|
||||
let language = languagePart.lowercased()
|
||||
|
||||
if parts.count >= 2, let regionPart = parts.last {
|
||||
return "\(language)_\(regionPart.uppercased())"
|
||||
}
|
||||
|
||||
return language
|
||||
}
|
||||
|
||||
private static func languageComponent(fromNormalizedKey key: String) -> String? {
|
||||
let components = key.split(separator: "_", omittingEmptySubsequences: true)
|
||||
guard let language = components.first else { return nil }
|
||||
return String(language)
|
||||
}
|
||||
|
||||
private static func variants(for key: String) -> [String] {
|
||||
var collected: [String] = []
|
||||
let underscore = key.replacingOccurrences(of: "-", with: "_")
|
||||
let hyphen = key.replacingOccurrences(of: "_", with: "-")
|
||||
|
||||
for candidate in Set([key, underscore, hyphen]) {
|
||||
collected.append(candidate)
|
||||
}
|
||||
|
||||
return collected
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -113,7 +230,7 @@ final class ComponentLibraryViewModel: ObservableObject {
|
||||
components?.queryItems = [
|
||||
URLQueryItem(name: "filter", value: "(type='load')"),
|
||||
URLQueryItem(name: "sort", value: "+name"),
|
||||
URLQueryItem(name: "fields", value: "id,collectionId,name,icon,voltage_in,voltage_out,watt"),
|
||||
URLQueryItem(name: "fields", value: "id,collectionId,name,translations,icon,voltage_in,voltage_out,watt"),
|
||||
URLQueryItem(name: "page", value: "\(page)"),
|
||||
URLQueryItem(name: "perPage", value: "\(perPage)")
|
||||
]
|
||||
@@ -153,6 +270,7 @@ final class ComponentLibraryViewModel: ObservableObject {
|
||||
ComponentLibraryItem(
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
translations: record.translations?.flattened ?? [:],
|
||||
voltageIn: record.voltageIn,
|
||||
voltageOut: record.voltageOut,
|
||||
watt: record.watt,
|
||||
@@ -300,6 +418,7 @@ final class ComponentLibraryViewModel: ObservableObject {
|
||||
let id: String
|
||||
let collectionId: String
|
||||
let name: String
|
||||
let translations: TranslationsContainer?
|
||||
let icon: String?
|
||||
let voltageIn: Double?
|
||||
let voltageOut: Double?
|
||||
@@ -309,11 +428,64 @@ final class ComponentLibraryViewModel: ObservableObject {
|
||||
case id
|
||||
case collectionId
|
||||
case name
|
||||
case translations
|
||||
case icon
|
||||
case voltageIn = "voltage_in"
|
||||
case voltageOut = "voltage_out"
|
||||
case watt
|
||||
}
|
||||
|
||||
struct TranslationsContainer: Decodable {
|
||||
private let storage: [String: TranslationValue]
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
storage = try container.decode([String: TranslationValue].self)
|
||||
}
|
||||
|
||||
var flattened: [String: String] {
|
||||
storage.reduce(into: [:]) { result, entry in
|
||||
if let value = entry.value.flattened {
|
||||
result[entry.key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum TranslationValue: Decodable {
|
||||
case string(String)
|
||||
case dictionary([String: String])
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let singleValue = try decoder.singleValueContainer()
|
||||
if let string = try? singleValue.decode(String.self) {
|
||||
self = .string(string)
|
||||
return
|
||||
}
|
||||
|
||||
if let dictionary = try? singleValue.decode([String: String].self) {
|
||||
self = .dictionary(dictionary)
|
||||
return
|
||||
}
|
||||
|
||||
self = .dictionary([:])
|
||||
}
|
||||
|
||||
var flattened: String? {
|
||||
switch self {
|
||||
case .string(let value):
|
||||
return value.isEmpty ? nil : value
|
||||
case .dictionary(let dictionary):
|
||||
if let name = dictionary["name"], !name.isEmpty {
|
||||
return name
|
||||
}
|
||||
if let value = dictionary["value"], !value.isEmpty {
|
||||
return value
|
||||
}
|
||||
return dictionary.values.first(where: { !$0.isEmpty })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AffiliateLinksResponse: Decodable {
|
||||
@@ -396,14 +568,17 @@ struct ComponentLibraryView: View {
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
} else {
|
||||
List(filteredItems) { item in
|
||||
Button {
|
||||
onSelect(item)
|
||||
dismiss()
|
||||
} label: {
|
||||
ComponentRow(item: item)
|
||||
List {
|
||||
ForEach(filteredItems) { item in
|
||||
Button {
|
||||
onSelect(item)
|
||||
dismiss()
|
||||
} label: {
|
||||
ComponentRow(item: item)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
poweredByVoltplanRow
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
@@ -414,7 +589,30 @@ struct ComponentLibraryView: View {
|
||||
guard !trimmedQuery.isEmpty else { return viewModel.items }
|
||||
|
||||
return viewModel.items.filter { item in
|
||||
item.name.localizedCaseInsensitiveContains(trimmedQuery)
|
||||
let localizedName = item.localizedName
|
||||
return localizedName.localizedCaseInsensitiveContains(trimmedQuery)
|
||||
|| item.name.localizedCaseInsensitiveContains(trimmedQuery)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var poweredByVoltplanRow: some View {
|
||||
if let url = URL(string: "https://voltplan.app") {
|
||||
Section {
|
||||
Link(destination: url) {
|
||||
Image("PoweredByVoltplan")
|
||||
.renderingMode(.original)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: 220)
|
||||
.padding(.vertical, 20)
|
||||
.frame(maxWidth: .infinity)
|
||||
.accessibilityLabel("Powered by Voltplan")
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
.textCase(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,7 +624,7 @@ private struct ComponentRow: View {
|
||||
HStack(spacing: 12) {
|
||||
iconView
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(item.name)
|
||||
Text(item.localizedName)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
detailLine
|
||||
|
||||
@@ -292,7 +292,8 @@ struct LoadsView: View {
|
||||
}
|
||||
|
||||
private func addComponent(_ item: ComponentLibraryItem) {
|
||||
let baseName = item.name.isEmpty ? "Library Load" : item.name
|
||||
let localizedName = item.localizedName
|
||||
let baseName = localizedName.isEmpty ? "Library Load" : localizedName
|
||||
let loadName = uniqueLoadName(startingWith: baseName)
|
||||
let voltage = item.displayVoltage ?? 12.0
|
||||
let power = item.watt ?? (item.current != nil ? item.current! * voltage : 0)
|
||||
|
||||
@@ -23,32 +23,34 @@ struct SystemsView: View {
|
||||
"pink", "teal", "indigo", "mint", "cyan", "brown", "gray"
|
||||
]
|
||||
private let defaultSystemIconName = "building.2"
|
||||
private let systemIconMappings: [(keywords: [String], icon: String)] = [
|
||||
(["rv", "van", "camper", "motorhome", "coach"], "bus"),
|
||||
(["truck", "trailer", "rig"], "truck.box"),
|
||||
(["boat", "marine", "yacht", "sail"], "sailboat"),
|
||||
(["plane", "air", "flight"], "airplane"),
|
||||
(["ferry", "ship"], "ferry"),
|
||||
(["house", "home", "cabin", "cottage", "lodge"], "house"),
|
||||
(["building", "office", "warehouse", "factory", "facility"], "building"),
|
||||
(["camp", "tent", "outdoor"], "tent"),
|
||||
(["solar", "sun"], "sun.max"),
|
||||
(["battery", "storage"], "battery.100"),
|
||||
(["server", "data", "network", "rack"], "server.rack"),
|
||||
(["computer", "electronics", "lab", "tech"], "cpu"),
|
||||
(["gear", "mechanic", "machine", "workshop"], "gear"),
|
||||
(["tool", "maintenance", "repair", "shop"], "wrench.adjustable"),
|
||||
(["hammer", "carpentry"], "hammer"),
|
||||
(["light", "lighting", "lamp"], "lightbulb"),
|
||||
(["bolt", "power", "electric"], "bolt"),
|
||||
(["plug"], "powerplug"),
|
||||
(["engine", "generator", "motor"], "engine.combustion"),
|
||||
(["fuel", "diesel", "gas"], "fuelpump"),
|
||||
(["water", "pump", "tank"], "drop"),
|
||||
(["heat", "heater", "furnace"], "flame"),
|
||||
(["cold", "freeze", "cool"], "snowflake"),
|
||||
(["climate", "hvac", "temperature"], "thermometer")
|
||||
]
|
||||
private var systemIconMappings: [(keywords: [String], icon: String)] {
|
||||
[
|
||||
(keywords(for: "system.icon.keywords.rv", fallback: ["rv", "van", "camper", "motorhome", "coach"]), "bus"),
|
||||
(keywords(for: "system.icon.keywords.truck", fallback: ["truck", "trailer", "rig"]), "truck.box"),
|
||||
(keywords(for: "system.icon.keywords.boat", fallback: ["boat", "marine", "yacht", "sail"]), "sailboat"),
|
||||
(keywords(for: "system.icon.keywords.plane", fallback: ["plane", "air", "flight"]), "airplane"),
|
||||
(keywords(for: "system.icon.keywords.ferry", fallback: ["ferry", "ship"]), "ferry"),
|
||||
(keywords(for: "system.icon.keywords.house", fallback: ["house", "home", "cabin", "cottage", "lodge"]), "house"),
|
||||
(keywords(for: "system.icon.keywords.building", fallback: ["building", "office", "warehouse", "factory", "facility"]), "building"),
|
||||
(keywords(for: "system.icon.keywords.tent", fallback: ["camp", "tent", "outdoor"]), "tent"),
|
||||
(keywords(for: "system.icon.keywords.solar", fallback: ["solar", "sun"]), "sun.max"),
|
||||
(keywords(for: "system.icon.keywords.battery", fallback: ["battery", "storage"]), "battery.100"),
|
||||
(keywords(for: "system.icon.keywords.server", fallback: ["server", "data", "network", "rack"]), "server.rack"),
|
||||
(keywords(for: "system.icon.keywords.computer", fallback: ["computer", "electronics", "lab", "tech"]), "cpu"),
|
||||
(keywords(for: "system.icon.keywords.gear", fallback: ["gear", "mechanic", "machine", "workshop"]), "gear"),
|
||||
(keywords(for: "system.icon.keywords.tool", fallback: ["tool", "maintenance", "repair", "shop"]), "wrench.adjustable"),
|
||||
(keywords(for: "system.icon.keywords.hammer", fallback: ["hammer", "carpentry"]), "hammer"),
|
||||
(keywords(for: "system.icon.keywords.light", fallback: ["light", "lighting", "lamp"]), "lightbulb"),
|
||||
(keywords(for: "system.icon.keywords.bolt", fallback: ["bolt", "power", "electric"]), "bolt"),
|
||||
(keywords(for: "system.icon.keywords.plug", fallback: ["plug"]), "powerplug"),
|
||||
(keywords(for: "system.icon.keywords.engine", fallback: ["engine", "generator", "motor"]), "engine.combustion"),
|
||||
(keywords(for: "system.icon.keywords.fuel", fallback: ["fuel", "diesel", "gas"]), "fuelpump"),
|
||||
(keywords(for: "system.icon.keywords.water", fallback: ["water", "pump", "tank"]), "drop"),
|
||||
(keywords(for: "system.icon.keywords.heat", fallback: ["heat", "heater", "furnace"]), "flame"),
|
||||
(keywords(for: "system.icon.keywords.cold", fallback: ["cold", "freeze", "cool"]), "snowflake"),
|
||||
(keywords(for: "system.icon.keywords.climate", fallback: ["climate", "hvac", "temperature"]), "thermometer")
|
||||
]
|
||||
}
|
||||
|
||||
private struct SystemNavigationTarget: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
@@ -224,7 +226,10 @@ struct SystemsView: View {
|
||||
}
|
||||
|
||||
private func createLoad(from item: ComponentLibraryItem, in system: ElectricalSystem) -> SavedLoad {
|
||||
let baseName = item.name.isEmpty ? String(localized: "default.load.library", comment: "Default name when importing a library load") : item.name
|
||||
let localizedName = item.localizedName
|
||||
let baseName = localizedName.isEmpty
|
||||
? String(localized: "default.load.library", comment: "Default name when importing a library load")
|
||||
: localizedName
|
||||
let loadName = uniqueLoadName(for: system, startingWith: baseName)
|
||||
let voltage = item.displayVoltage ?? 12.0
|
||||
|
||||
@@ -352,6 +357,32 @@ struct SystemsView: View {
|
||||
return defaultSystemIconName
|
||||
}
|
||||
|
||||
private func keywords(for localizationKey: String, fallback: [String]) -> [String] {
|
||||
let fallbackValue = fallback.joined(separator: ",")
|
||||
let localizedKeywords = NSLocalizedString(
|
||||
localizationKey,
|
||||
tableName: nil,
|
||||
bundle: .main,
|
||||
value: fallbackValue,
|
||||
comment: ""
|
||||
)
|
||||
let separators = CharacterSet(charactersIn: ",;")
|
||||
let components = localizedKeywords
|
||||
.components(separatedBy: separators)
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
|
||||
.filter { !$0.isEmpty }
|
||||
|
||||
var uniqueKeywords: [String] = []
|
||||
|
||||
for keyword in fallback.map({ $0.lowercased() }) + components {
|
||||
if !uniqueKeywords.contains(keyword) {
|
||||
uniqueKeywords.append(keyword)
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueKeywords
|
||||
}
|
||||
|
||||
private func colorForName(_ colorName: String) -> Color {
|
||||
switch colorName {
|
||||
case "blue": return .blue
|
||||
|
||||
@@ -44,6 +44,30 @@
|
||||
"sample.load.lighting.name" = "LED-Streifenbeleuchtung";
|
||||
"sample.load.compressor.name" = "Luftkompressor";
|
||||
"sample.load.charger.name" = "Werkzeugladegerät";
|
||||
"system.icon.keywords.rv" = "wohnmobil, camper, campervan, van, reisemobil, campingbus";
|
||||
"system.icon.keywords.truck" = "lkw, anhänger, zugmaschine, trailer";
|
||||
"system.icon.keywords.boat" = "boot, schiff, yacht, segel, segelboot";
|
||||
"system.icon.keywords.plane" = "flugzeug, flug, flieger, luft";
|
||||
"system.icon.keywords.ferry" = "fähre, schiff";
|
||||
"system.icon.keywords.house" = "haus, zuhause, hütte, ferienhaus, lodge";
|
||||
"system.icon.keywords.building" = "gebäude, büro, lagerhalle, fabrik, anlage";
|
||||
"system.icon.keywords.tent" = "camp, camping, zelt, outdoor";
|
||||
"system.icon.keywords.solar" = "solar, sonne, pv";
|
||||
"system.icon.keywords.battery" = "batterie, speicher, akku";
|
||||
"system.icon.keywords.server" = "server, daten, netzwerk, rack, rechenzentrum";
|
||||
"system.icon.keywords.computer" = "computer, elektronik, labor, technik";
|
||||
"system.icon.keywords.gear" = "getriebe, mechanik, maschine, werkstatt";
|
||||
"system.icon.keywords.tool" = "werkzeug, wartung, reparatur, werkstatt";
|
||||
"system.icon.keywords.hammer" = "hammer, tischlerei, zimmerei";
|
||||
"system.icon.keywords.light" = "licht, beleuchtung, lampe";
|
||||
"system.icon.keywords.bolt" = "strom, power, elektrisch, spannung";
|
||||
"system.icon.keywords.plug" = "stecker, netzstecker";
|
||||
"system.icon.keywords.engine" = "motor, generator, antrieb";
|
||||
"system.icon.keywords.fuel" = "kraftstoff, diesel, benzin";
|
||||
"system.icon.keywords.water" = "wasser, pumpe, tank";
|
||||
"system.icon.keywords.heat" = "heizung, heizer, ofen";
|
||||
"system.icon.keywords.cold" = "kalt, kühlen, eis, gefrieren";
|
||||
"system.icon.keywords.climate" = "klima, hvac, temperatur";
|
||||
|
||||
// Direct strings
|
||||
"Systems" = "Systeme";
|
||||
@@ -52,9 +76,9 @@
|
||||
"System Name" = "Systemname";
|
||||
"Create System" = "System erstellen";
|
||||
"Create your first system" = "Erstelle dein erstes System";
|
||||
"Give your setup a name so **Cable by VoltPlan** can organize loads, wiring, and recommendations in one place." = "Gib deinem System einen Namen, damit **Cable by VoltPlan** alle zusammengehörenden Verbraucher gruppieren kann.";
|
||||
"Give your setup a name so **Cable by VoltPlan** can organize loads, wiring, and recommendations in one place." = "Ob \"**Mein System**\", \"**Motoryacht LARGO**\" oder \"**Camper Luigi**\". Dein System braucht einen Namen.";
|
||||
"Add your first component" = "Erstelle deine erste Komponente";
|
||||
"Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations." = "Erwecke dein System mit Komponenten zum Leben und überlasse **Cable by VoltPlan** die Kabel- und Sicherungsempfehlungen.";
|
||||
"Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations." = "Komponenten sind das Herz von Cable. Stelle sicher, dass du nie wieder ein zu dünnes Kabel oder die falsche Sicherung verwendest.";
|
||||
"Create Component" = "Komponente erstellen";
|
||||
"Browse Library" = "Bibliothek durchsuchen";
|
||||
"Browse" = "Durchsuchen";
|
||||
|
||||
@@ -44,6 +44,30 @@
|
||||
"sample.load.lighting.name" = "Iluminación LED";
|
||||
"sample.load.compressor.name" = "Compresor de aire";
|
||||
"sample.load.charger.name" = "Cargador de herramientas";
|
||||
"system.icon.keywords.rv" = "autocaravana, camper, caravana, furgo, van";
|
||||
"system.icon.keywords.truck" = "camión, remolque, tráiler";
|
||||
"system.icon.keywords.boat" = "barco, embarcación, yate, vela";
|
||||
"system.icon.keywords.plane" = "avión, vuelo, aire";
|
||||
"system.icon.keywords.ferry" = "ferry, transbordador, barco";
|
||||
"system.icon.keywords.house" = "casa, hogar, cabaña, chalet";
|
||||
"system.icon.keywords.building" = "edificio, oficina, almacén, fábrica, instalación";
|
||||
"system.icon.keywords.tent" = "camping, tienda, exterior";
|
||||
"system.icon.keywords.solar" = "solar, sol, fotovoltaico";
|
||||
"system.icon.keywords.battery" = "batería, almacenamiento, acumulador";
|
||||
"system.icon.keywords.server" = "servidor, datos, red, rack";
|
||||
"system.icon.keywords.computer" = "computadora, ordenador, electrónica, laboratorio, tecnología";
|
||||
"system.icon.keywords.gear" = "engranaje, mecánica, máquina, taller";
|
||||
"system.icon.keywords.tool" = "herramienta, mantenimiento, reparación, taller";
|
||||
"system.icon.keywords.hammer" = "martillo, carpintería";
|
||||
"system.icon.keywords.light" = "luz, iluminación, lámpara";
|
||||
"system.icon.keywords.bolt" = "volt, energía, eléctrico, potencia";
|
||||
"system.icon.keywords.plug" = "enchufe, clavija";
|
||||
"system.icon.keywords.engine" = "motor, generador";
|
||||
"system.icon.keywords.fuel" = "combustible, diésel, gasolina";
|
||||
"system.icon.keywords.water" = "agua, bomba, tanque, depósito";
|
||||
"system.icon.keywords.heat" = "calor, calefacción, horno";
|
||||
"system.icon.keywords.cold" = "frío, congelar, enfriar";
|
||||
"system.icon.keywords.climate" = "clima, hvac, temperatura";
|
||||
|
||||
// Direct strings
|
||||
"Systems" = "Sistemas";
|
||||
|
||||
@@ -44,6 +44,30 @@
|
||||
"sample.load.lighting.name" = "Éclairage LED";
|
||||
"sample.load.compressor.name" = "Compresseur d'air";
|
||||
"sample.load.charger.name" = "Chargeur d'outils";
|
||||
"system.icon.keywords.rv" = "camping-car, van, fourgon, caravane, motorhome";
|
||||
"system.icon.keywords.truck" = "camion, remorque, poids lourd";
|
||||
"system.icon.keywords.boat" = "bateau, marine, yacht, voile";
|
||||
"system.icon.keywords.plane" = "avion, vol, air";
|
||||
"system.icon.keywords.ferry" = "ferry, traversier, bateau";
|
||||
"system.icon.keywords.house" = "maison, foyer, cabane, chalet, lodge";
|
||||
"system.icon.keywords.building" = "bâtiment, bureau, entrepôt, usine, installation";
|
||||
"system.icon.keywords.tent" = "camping, tente, plein air";
|
||||
"system.icon.keywords.solar" = "solaire, soleil";
|
||||
"system.icon.keywords.battery" = "batterie, stockage, accumulateur";
|
||||
"system.icon.keywords.server" = "serveur, données, réseau, rack";
|
||||
"system.icon.keywords.computer" = "ordinateur, électronique, labo, techno";
|
||||
"system.icon.keywords.gear" = "engrenage, mécanique, machine, atelier";
|
||||
"system.icon.keywords.tool" = "outil, maintenance, réparation, atelier";
|
||||
"system.icon.keywords.hammer" = "marteau, charpente";
|
||||
"system.icon.keywords.light" = "lumière, éclairage, lampe";
|
||||
"system.icon.keywords.bolt" = "courant, énergie, électrique";
|
||||
"system.icon.keywords.plug" = "prise, fiche";
|
||||
"system.icon.keywords.engine" = "moteur, générateur";
|
||||
"system.icon.keywords.fuel" = "carburant, diesel, essence";
|
||||
"system.icon.keywords.water" = "eau, pompe, réservoir";
|
||||
"system.icon.keywords.heat" = "chaleur, chauffage, chaudière, four";
|
||||
"system.icon.keywords.cold" = "froid, geler, refroidir";
|
||||
"system.icon.keywords.climate" = "climat, hvac, température";
|
||||
|
||||
// Direct strings
|
||||
"Systems" = "Systèmes";
|
||||
|
||||
@@ -44,6 +44,30 @@
|
||||
"sample.load.lighting.name" = "LED-strips";
|
||||
"sample.load.compressor.name" = "Luchtcompressor";
|
||||
"sample.load.charger.name" = "Gereedschapslader";
|
||||
"system.icon.keywords.rv" = "camper, kampeerbus, buscamper, mobilhome, campervan";
|
||||
"system.icon.keywords.truck" = "vrachtwagen, trailer, aanhanger, truck";
|
||||
"system.icon.keywords.boat" = "boot, schip, jacht, zeil";
|
||||
"system.icon.keywords.plane" = "vliegtuig, vlucht, lucht";
|
||||
"system.icon.keywords.ferry" = "veerboot, ferry, schip";
|
||||
"system.icon.keywords.house" = "huis, thuis, hut, chalet, lodge";
|
||||
"system.icon.keywords.building" = "gebouw, kantoor, magazijn, fabriek, faciliteit";
|
||||
"system.icon.keywords.tent" = "kamperen, tent, buiten";
|
||||
"system.icon.keywords.solar" = "zonne, zon, zonnepaneel";
|
||||
"system.icon.keywords.battery" = "batterij, opslag, accu";
|
||||
"system.icon.keywords.server" = "server, data, netwerk, rack";
|
||||
"system.icon.keywords.computer" = "computer, elektronica, lab, tech";
|
||||
"system.icon.keywords.gear" = "tandwiel, mechanica, machine, werkplaats";
|
||||
"system.icon.keywords.tool" = "gereedschap, onderhoud, reparatie, werkplaats";
|
||||
"system.icon.keywords.hammer" = "hamer, timmerwerk";
|
||||
"system.icon.keywords.light" = "licht, verlichting, lamp";
|
||||
"system.icon.keywords.bolt" = "stroom, kracht, elektrisch, spanning";
|
||||
"system.icon.keywords.plug" = "stekker, aansluiting";
|
||||
"system.icon.keywords.engine" = "motor, generator";
|
||||
"system.icon.keywords.fuel" = "brandstof, diesel, benzine";
|
||||
"system.icon.keywords.water" = "water, pomp, tank, reservoir";
|
||||
"system.icon.keywords.heat" = "warmte, verwarming, kachel";
|
||||
"system.icon.keywords.cold" = "koud, vries, koel";
|
||||
"system.icon.keywords.climate" = "klimaat, hvac, temperatuur";
|
||||
|
||||
// Direct strings
|
||||
"Systems" = "Systemen";
|
||||
|
||||