44 Commits

Author SHA1 Message Date
239deb2b91 RES-32 Ajouter la possibilité désactiver des traqueurs spécifiques
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2025-02-12 10:49:06 +01:00
9a05ce29b8 Disable tag for previews
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-12-17 16:30:22 +01:00
2ec4ebcc66 Fix plugin swiftlint
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-11-04 14:00:27 +01:00
6ea31a8030 Fix some error when value and replaceIn
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-07-18 11:36:21 +02:00
df173406d4 Update README.md
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-07-18 11:36:11 +02:00
922ed56959 Add parameter defaultValue and value and add replaceIn parameter in another parameter
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-07-18 08:24:23 +02:00
ab91c1c277 Update README.md
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-07-17 15:27:10 +02:00
aa64ce5cf7 fix: Static function 2024-07-17 15:26:39 +02:00
23bf3c3a82 fix: Use category and action if not matomo 2024-07-17 15:26:39 +02:00
09556ba6e0 [DEVTOOLS-186] Exporter les images de resgen en svg
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #12
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-07-17 15:18:13 +02:00
dea57dc1e2 Maj du readme
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-07-11 10:29:32 +02:00
07575bd2bf DEVTOOLS-195 Ne pas générer les svg en template par défaut
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-07-11 10:08:47 +02:00
8686ae974c Suppression des anciens assets si svg
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-06-21 14:54:20 +02:00
be4c561ea8 DEVTOOLS-192 Resgen iOS vector
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-06-21 09:18:51 +02:00
2357a40fff DEVTOOLS-186 Exporter les images de resgen en svg
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-04-22 12:05:24 +02:00
d4afa9c9e9 Maj du Readme suite aux devs sur le fichier string catalog
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-22 09:48:21 +02:00
76ef0a2d59 Merge pull request 'DEVTOOLS-185 Remplacer le json en dur des images resgen' (#11) from DEVTOOLS-186/SVG_resgen into master
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #11
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-04-19 17:02:26 +02:00
129eb135f1 DEVTOOLS-185 Remplacer le json en dur des images resgen
Some checks are pending
gitea-openium/resgen.swift/pipeline/pr-master Build started...
2024-04-19 17:02:00 +02:00
4ad15fcded Merge pull request 'xcstrings' (#10) from xcstrings into master
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #10
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-04-19 16:59:03 +02:00
fb2ddb2227 DEVTOOLS-181 Gérer le tag noTranslation pour les xcstrings 2024-04-17 09:44:09 +02:00
27f86f5c4d Correction de l'option pour le fichier xcstrings 2024-04-15 16:51:18 +02:00
209ba49e3f Gestion des commentaires 2024-04-12 17:15:15 +02:00
ba07005b13 Fix equatable properties for arrays 2024-04-12 16:45:09 +02:00
6c3f3a8982 Correction du test testGenerateXcStringsRootObject 2024-04-12 16:25:10 +02:00
0d651b810f Première implémentation des xcstrings 2024-04-12 16:09:54 +02:00
1d7fc76340 Changed the condition for moe visibility 2024-04-11 15:49:07 +02:00
5d4e461933 Affichage du commentaire même si nil ou empty 2024-04-11 14:20:54 +02:00
55264d61ad Modification de l'affichage des commentaires
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-04-11 11:18:57 +02:00
d21ad9d1ea Update JenkinsFile Xcode version
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-11 10:19:10 +02:00
0bd6c3c2d4 Correction des commentaires des strings
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-11 10:16:28 +02:00
eed20367b9 fix: Edit for NSObject in Firebase
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
PixeeBox/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 11:24:21 +01:00
43b5111d79 Actualiser Sources/ResgenSwift/Analytics/Generator/AnalyticsGenerator.swift
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 10:39:12 +01:00
2983093a9c Add Swiftlint 2023-12-13 10:39:12 +01:00
b4bbaa3bfd Fix Image 2023-12-13 10:39:12 +01:00
498c8fa4ae Fix Font 2023-12-13 10:39:12 +01:00
2957da6233 Fix Color 2023-12-13 10:39:12 +01:00
d79af06c38 docs: Add Analytics section in README
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 10:27:35 +01:00
d8937f2de6 docs: Add Analytics section in README 2023-12-13 10:23:31 +01:00
9b27f24197 docs: Add Analytics section in README 2023-12-13 10:20:53 +01:00
1d58fd5510 Edit sample file
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2023-12-12 16:58:25 +01:00
f6c49bf626 Add parse error
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 11:19:19 +01:00
f1b62d83c4 Add missing print error
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:29:19 +01:00
ee5055efa5 Retours sur la structure du code
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:24:42 +01:00
6f8e3b6664 Add error handling
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:09:24 +01:00
67 changed files with 3299 additions and 637 deletions

View File

@ -13,7 +13,7 @@
- Update plist `UIAppFonts` when generated fonts (use plistBuddy) - Update plist `UIAppFonts` when generated fonts (use plistBuddy)
- New parameter: `infoPlistPaths` - New parameter: `infoPlistPaths`
- Generate SwiftUI extensions for colors, fonts and images - Generate SwiftUI extensions for colors, fonts and images
- New parameter: `extensionNameSwiftUI` - New parameter: `extensionNameUIKit`
- Adding Makefile to install, unsintall and create man page. - Adding Makefile to install, unsintall and create man page.
## Fixes ## Fixes

2
Jenkinsfile vendored
View File

@ -1,6 +1,6 @@
library "openiumpipeline" library "openiumpipeline"
env.DEVELOPER_DIR="/Applications/Xcode-15.0.1.app/Contents/Developer" env.DEVELOPER_DIR="/Applications/Xcode-15.4.0.app/Contents/Developer"
//env.SIMULATOR_DEVICE_TYPES="iPad--7th-generation-" //env.SIMULATOR_DEVICE_TYPES="iPad--7th-generation-"
env.IS_PACKAGE_SWIFT=1 env.IS_PACKAGE_SWIFT=1
env.TARGETS_MACOS=1 env.TARGETS_MACOS=1

View File

@ -5,8 +5,17 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser", "location" : "https://github.com/apple/swift-argument-parser",
"state" : { "state" : {
"revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1", "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
"version" : "1.1.4" "version" : "1.2.3"
}
},
{
"identity" : "swiftlintplugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/lukepistrol/SwiftLintPlugin",
"state" : {
"revision" : "5a65f4074975f811da666dfe31a19850950b1ea4",
"version" : "0.56.2"
} }
}, },
{ {
@ -14,8 +23,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git", "location" : "https://github.com/jpsim/Yams.git",
"state" : { "state" : {
"revision" : "01835dc202670b5bb90d07f3eae41867e9ed29f6", "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3",
"version" : "5.0.1" "version" : "5.0.6"
} }
} }
], ],

View File

@ -9,7 +9,8 @@ let package = Package(
dependencies: [ dependencies: [
// Dependencies declare other packages that this package depends on. // Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.1") .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.1"),
.package(url: "https://github.com/lukepistrol/SwiftLintPlugin", exact: "0.56.2"),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets are the basic building blocks of a package. A target can define a module or a test suite.
@ -20,6 +21,9 @@ let package = Package(
"ToolCore", "ToolCore",
.product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "ArgumentParser", package: "swift-argument-parser"),
"Yams" "Yams"
],
plugins: [
.plugin(name: "SwiftLint", package: "SwiftLintPlugin")
] ]
), ),

173
README.md
View File

@ -16,7 +16,7 @@ iOS required to use the **real name** of the font, this name can be different fr
swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/fonts.txt" \ swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/fonts.txt" \
--extension-output-path "./Fonts/Generated" \ --extension-output-path "./Fonts/Generated" \
--extension-name "AppFont" \ --extension-name "AppFont" \
--extension-name-swift-ui "SUIAppFont" \ --extension-name-ui-kit "UIAppFont" \
--extension-suffix "GreatApp" \ --extension-suffix "GreatApp" \
--static-members true \ --static-members true \
--info-plist-paths "./path/one/to/Info.plist ./path/two/to/Info.plist" --info-plist-paths "./path/one/to/Info.plist ./path/two/to/Info.plist"
@ -27,8 +27,8 @@ swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/fonts.txt" \
1. `-f`: force generation 1. `-f`: force generation
2. Font input folder, it will search for every `.ttf` and `.otf` files specified in `fonts.txt` 2. Font input folder, it will search for every `.ttf` and `.otf` files specified in `fonts.txt`
3. `--extension-output-path`: path where to generate generated extension 3. `--extension-output-path`: path where to generate generated extension
4. `--extension-name` *(optional)* : name of the class to add UIKit getters 4. `--extension-name` *(optional)* : name of the class to add SwiftUI getters
5. `--extension-name-swift-ui` *(optional)* : name of the class to add SwiftUI getters 5. `--extension-name-ui-kit` *(optional)* : name of the class to add UIKit getters
6. `--extension-suffix` *(optional)* : additional text which is added to the filename (ex: `AppFont+GreatApp.swift`) 6. `--extension-suffix` *(optional)* : additional text which is added to the filename (ex: `AppFont+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not 7. `--static-members` *(optional)*: generate static properties or not
8. `--info-plist-paths` *(optional)*: array of `.plist`, you can specify multiple `Info.plist` for multiple targets 8. `--info-plist-paths` *(optional)*: array of `.plist`, you can specify multiple `Info.plist` for multiple targets
@ -44,7 +44,7 @@ swift run -c release ResgenSwift colors $FORCE_FLAG "./Colors/colors.txt" \
--xcassets-path "./Colors/colors.xcassets" \ --xcassets-path "./Colors/colors.xcassets" \
--extension-output-path "./Colors/Generated/" \ --extension-output-path "./Colors/Generated/" \
--extension-name "AppColor" \ --extension-name "AppColor" \
--extension-name-swift-ui "SUIAppColor" \ --extension-name-ui-kit "UIAppColor" \
--extension-suffix "GreatApp" \ --extension-suffix "GreatApp" \
--static-members true --static-members true
``` ```
@ -55,8 +55,8 @@ swift run -c release ResgenSwift colors $FORCE_FLAG "./Colors/colors.txt" \
2. Input colors file 2. Input colors file
3. `--style` can be `all` or `light` 3. `--style` can be `all` or `light`
4. `--extension-output-path`: path where to generate generated extension 4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of the class to add UIKit getters 5. `--extension-name` *(optional)* : name of the class to add SwiftUI getters
6. `--extension-name-swift-ui` *(optional)* : name of the class to add SwiftUI getters 6. `--extension-name-ui-kit` *(optional)* : name of the class to add UIKit getters
7. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppColor+GreatApp.swift`) 7. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppColor+GreatApp.swift`)
8. `--static-members` *(optional)*: generate static properties or not 8. `--static-members` *(optional)*: generate static properties or not
@ -81,7 +81,7 @@ swift run -c release ResgenSwift strings twine $FORCE_FLAG "./Twine/strings.txt"
2. Input translations file (must be Twine formatted) 2. Input translations file (must be Twine formatted)
3. `--langs`: langs to generate (string with space between each lang) 3. `--langs`: langs to generate (string with space between each lang)
4. `--default-lang`: default lang that will be in `Base.lproj`. It must be in `langs` as well 4. `--default-lang`: default lang that will be in `Base.lproj`. It must be in `langs` as well
4. `--extension-output-path`: path where to generate generated extension 5. `--extension-output-path`: path where to generate generated extension
### Stringium (recommended) ### Stringium (recommended)
@ -93,6 +93,7 @@ swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/string
--extension-output-path "./Strings/Generated" \ --extension-output-path "./Strings/Generated" \
--extension-name "AppString" \ --extension-name "AppString" \
--extension-suffix "GreatApp" \ --extension-suffix "GreatApp" \
--xcStrings true
--static-members true --static-members true
``` ```
@ -105,6 +106,7 @@ swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/string
4. `--extension-output-path`: path where to generate generated extension 4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of class to add the extension 5. `--extension-name` *(optional)* : name of class to add the extension
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppString+GreatApp.swift`) 6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppString+GreatApp.swift`)
6. `--xcStrings`*(optional)* : generate string catalog
7. `--static-members` *(optional)*: generate static properties or not 7. `--static-members` *(optional)*: generate static properties or not
@ -131,7 +133,136 @@ swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/tags.txt" \
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppTags+GreatApp.swift`) 6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppTags+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not 7. `--static-members` *(optional)*: generate static properties or not
> ⚠️ If extension name is not set or is `Tags`, it will generate the following typaloas `typealias Tags = String`. > ⚠️ If extension name is not set or is `Tags`, it will generate the following typealias `typealias Tags = String`.
## Analytics
Analytics will generate all you need to analyze UX with Matomo or Firebase Analytics. Input files are formatted in YAML. This command will generate a manager for each target and an AnalyticsManager. This is this one you will need to use. And it will generate a method for all tags you have declared in the YAML file. Next, you will need to use the `configure()` method of AnalyticsManager and if you want to use matomo to set up the `siteId` and the `url` of the site.
```sh
swift run -c release ResgenSwift analytics $FORCE_FLAG "./Tags/analytics.yml" \
--target "matomo firebase" \
--extension-output-path "./Analytics/Generated" \
--extension-name "AppAnalytics" \
--extension-suffix "GreatApp" \
--static-members true
```
**Parameters**
1. `-f`: force generation
2. Input tags file (must be YAML formatted)
3. `--target`: target with you will log UX
4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of class to add the extension
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppAnalytics+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not
> ⚠️ If extension name is not set or is `Analytics`, it will generate the following typealias `typealias Analytics = String`.
### YAML
```
- id: s1_def_one
name: s1 def one _TITLE_
path: s1_def_one/_TITLE_
action: Tap
category: User
tags: ios,droid
comments:
parameters:
- name: title
type: String
replaceIn: name,path
```
1. `id`: name of the method (method name will be composed of `log` + `Event|Screen` + `id`)
2. `name`: name of the tag
3. `path` *(optional with firebase)* : needed for matomo but not with firebase (log screen)
4. `action` *(optional with firebase)* : needed for matomo but not with firebase (log event)
5. `category` *(optional with firebase)* : needed for matomo but not with firebase (log event)
6. `tags`: which platform target
7. `comments` *(optional)*
8. `parameters` *(optional)*
**Parameters**
You can use parameters in generate methods.
1. `name`: name of the parameter
2. `type`: type of the parameter (Int, String, Bool, Double)
3. `value`: value of the parameter
4. `defaultValue`: defaultValue of the parameter
3. `replaceIn` *(optional)*
**Value**
If you want to send another parameter with a static value. For example, you want to send to which screen the event is triggered.
You can add the parameter 'screenName' for example and its 'value' is 'Home'. With this, you do not need to specify the value in the function call.
**DefaultValue**
If you want ta add a parameter in the call of the function but you want to make it optionnal with a default value you need to use this property.
Example:
```
events:
id: id_of_tag
name: _TITLE_
tags: ios,droid
parameters:
- name: title
type: String
defaultValue: someTitle
```
The generated method will be:
```
logIdOfTag(title: String = "someTitle")
```
**Replace in**
This is section is equivalent of `%s | %d | %f | %@`. You can put the content of the parameter in *name*, *path*, *action*, *category*.
You need to put `_` + `NAME OF THE PARAMETER` + `_` in the target and which target you want in the value of `replaceIn`. (name need to be in uppercase).
You can't use `value`and `replaceIn`in thge same time.
Example:
```
events:
id: id_of_tag
name: _TITLE_
tags: ios,droid
parameters:
- name: title
type: String
replaceIn: name
```
In this sample, we want to add the parameter `title` in the field `name`. So, we need to place `_TITLE_` in the field `name`.
The generated method will be:
```
logIdOfTag(title: String)
```
You can also want to replace a parameter in an other parameter. You can do this with the `replaceIn` property. The condition is that the parameter which will use the `replaceIn`need to have the `value`property
Example:
```
events:
id: id_of_tag
name: title
tags: ios,droid
parameters:
- name: something
type: String
value: test _TEXT_
- name: text
type: String
replaceIn: something
```
## Images ## Images
@ -142,7 +273,7 @@ swift run -c release ResgenSwift images $FORCE_FLAG "./Images/images.txt" \
--xcassets-path "./Images/app.xcassets" \ --xcassets-path "./Images/app.xcassets" \
--extension-output-path "./Images/Generated" \ --extension-output-path "./Images/Generated" \
--extension-name "AppImage" \ --extension-name "AppImage" \
--extension-name-swift-ui "SUIAppImage" \ --extension-name-ui-kit "UIAppImage" \
--extension-suffix "GreatApp" \ --extension-suffix "GreatApp" \
--static-members true --static-members true
``` ```
@ -153,11 +284,12 @@ swift run -c release ResgenSwift images $FORCE_FLAG "./Images/images.txt" \
2. Input images definitions file 2. Input images definitions file
3. `--xcassets-path`: xcasset path where to generate imagesets 3. `--xcassets-path`: xcasset path where to generate imagesets
4. `--extension-output-path`: path where to generate generated extension 4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of the class to add UIKit getters 5. `--extension-name` *(optional)* : name of the class to add SwiftUI getters
6. `--extension-name-swift-ui` *(optional)* : name of the class to add SwiftUI getters 6. `--extension-name-ui-kit` *(optional)* : name of the class to add UIKit getters
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppImage+GreatApp.swift`) 6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppImage+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not 7. `--static-members` *(optional)*: generate static properties or not
> ⚠️ Svg images will be copied in the assets and rendered as "Original", however if those images are not rendered correctly you can force the png generation by adding the key word "png" like this: id arrow_back 15 ? png
## All at once ## All at once
@ -176,7 +308,7 @@ colors:
xcassetsPath: String xcassetsPath: String
extensionOutputPath: String extensionOutputPath: String
extensionName: String? extensionName: String?
extensionNameSwiftUI: String? extensionNameUIKit: String?
extensionSuffix: String? extensionSuffix: String?
staticMembers: Bool? staticMembers: Bool?
@ -185,7 +317,7 @@ fonts:
inputFile: String inputFile: String
extensionOutputPath: String extensionOutputPath: String
extensionName: String? extensionName: String?
extensionNameSwiftUI: String? extensionNameUIKit: String?
extensionSuffix: String? extensionSuffix: String?
staticMembers: Bool? staticMembers: Bool?
infoPlistPaths: [String] infoPlistPaths: [String]
@ -196,7 +328,7 @@ images:
xcassetsPath: String xcassetsPath: String
extensionOutputPath: String extensionOutputPath: String
extensionName: String? extensionName: String?
extensionNameSwiftUI: String? extensionNameUIKit: String?
extensionSuffix: String? extensionSuffix: String?
staticMembers: Bool? staticMembers: Bool?
@ -219,6 +351,15 @@ tags:
extensionName: String? extensionName: String?
extensionSuffix: String? extensionSuffix: String?
staticMembers: Bool? staticMembers: Bool?
analytics:
-
inputFile: String
target: String
extensionOutputPath: String
extensionName: String?
extensionSuffix: String?
staticMembers: Bool?
``` ```
### Multiple configurations ### Multiple configurations
@ -236,7 +377,7 @@ colors:
xcassetsPath: String xcassetsPath: String
extensionOutputPath: String extensionOutputPath: String
extensionName: String? extensionName: String?
extensionNameSwiftUI: String? extensionNameUIKit: String?
extensionSuffix: String? extensionSuffix: String?
staticMembers: Bool? staticMembers: Bool?
- -
@ -245,7 +386,7 @@ colors:
xcassetsPath: String xcassetsPath: String
extensionOutputPath: String extensionOutputPath: String
extensionName: String? extensionName: String?
extensionNameSwiftUI: String? extensionNameUIKit: String?
extensionSuffix: String? extensionSuffix: String?
staticMembers: Bool? staticMembers: Bool?
... ...

View File

@ -1,18 +1,25 @@
// Generated by ResgenSwift.Analytics 1.2 // Generated by ResgenSwift.Analytics 1.2
import MatomoTracker import MatomoTracker
import Firebase import FirebaseAnalytics
// MARK: - Protocol // MARK: - Protocol
protocol AnalyticsManagerProtocol { protocol AnalyticsManagerProtocol {
func logScreen(name: String, path: String) func logScreen(
name: String,
path: String,
params: [String: Any]?
)
func logEvent( func logEvent(
name: String, name: String,
action: String, action: String,
category: String, category: String,
params: [String: Any]? params: [String: Any]?
) )
func setEnable(_ enable: Bool)
} }
// MARK: - Matomo // MARK: - Matomo
@ -47,8 +54,11 @@ class MatomoAnalyticsManager: AnalyticsManagerProtocol {
// MARK: - Methods // MARK: - Methods
func logScreen(name: String, path: String) { func logScreen(
guard !tracker.isOptedOut else { return } name: String,
path: String,
params: [String: Any]?
) {
guard let trackerUrl = tracker.contentBase?.absoluteString else { return } guard let trackerUrl = tracker.contentBase?.absoluteString else { return }
let urlString = URL(string: "\(trackerUrl)" + "/" + "\(path)" + "iOS") let urlString = URL(string: "\(trackerUrl)" + "/" + "\(path)" + "iOS")
@ -64,8 +74,6 @@ class MatomoAnalyticsManager: AnalyticsManagerProtocol {
category: String, category: String,
params: [String: Any]? params: [String: Any]?
) { ) {
guard !tracker.isOptedOut else { return }
tracker.track( tracker.track(
eventWithCategory: category, eventWithCategory: category,
action: action, action: action,
@ -74,16 +82,40 @@ class MatomoAnalyticsManager: AnalyticsManagerProtocol {
url: nil url: nil
) )
} }
func setEnable(_ enable: Bool) {
tracker.isOptedOut = !enable
}
} }
// MARK: - Firebase // MARK: - Firebase
class FirebaseAnalyticsManager: AnalyticsManagerProtocol { class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
func logScreen(name: String, path: String) { func logScreen(
name: String,
path: String,
params: [String: Any]?
) {
var parameters = [ var parameters = [
AnalyticsParameterScreenName: name AnalyticsParameterScreenName: name as NSObject
] ]
if path.isEmpty == false {
parameters["path"] = path + "/iOS" as NSObject
}
if let supplementaryParameters = params {
for (newKey, newValue) in supplementaryParameters {
if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
}
}
Analytics.logEvent( Analytics.logEvent(
AnalyticsEventScreenView, AnalyticsEventScreenView,
parameters: parameters parameters: parameters
@ -96,22 +128,38 @@ class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
category: String, category: String,
params: [String: Any]? params: [String: Any]?
) { ) {
var parameters: [String:Any] = [ var parameters: [String:NSObject] = [
"action": action, AnalyticsParameterItemName: name.replacingOccurrences(of: " ", with: "_") as NSObject
"category": category,
] ]
if category.isEmpty == false {
parameters["AnalyticsParameterItemCategory"] = category as NSObject
}
if action.isEmpty == false {
parameters["action"] = action as NSObject
}
if let supplementaryParameters = params { if let supplementaryParameters = params {
parameters.merge(supplementaryParameters) { (origin, new) -> Any in for (newKey, newValue) in supplementaryParameters {
return origin if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
} }
} }
Analytics.logEvent( Analytics.logEvent(
name, AnalyticsEventSelectContent,
parameters: parameters parameters: parameters
) )
} }
func setEnable(_ enable: Bool) {
Analytics.setAnalyticsCollectionEnabled(enable)
}
} }
// MARK: - Manager // MARK: - Manager
@ -121,31 +169,57 @@ class AnalyticsManager {
// MARK: - Properties // MARK: - Properties
var managers: [AnalyticsManagerProtocol] = [] var managers: [TargetType: AnalyticsManagerProtocol] = []
private var isEnabled: Bool = true private var isEnabled: Bool {
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
false
} else {
true
}
}
// MARK: - Methods // MARK: - Methods
func setAnalyticsEnabled(_ enable: Bool) { private func setAnalytics(enable: Bool) {
isEnabled = enable managers.forEach { (key, value) in
if analytics.contains(where: { type in
type == key
}) {
value.setEnable(enable)
}
}
}
func enableAnalytics(_ analytics: [TargetType] = TargetType.allCases) {
setAnalytics(enable: true)
}
func disableAnalytics(_ analytics: [TargetType] = TargetType.allCases) {
setAnalytics(enable: false)
} }
func configure(siteId: String, url: String) { func configure(siteId: String, url: String) {
managers.append( managers[TrackerType.matomo] = MatomoAnalyticsManager(
MatomoAnalyticsManager(
siteId: siteId, siteId: siteId,
url: url url: url
) )
) managers[TrackerType.firebase] = FirebaseAnalyticsManager()
managers.append(FirebaseAnalyticsManager())
} }
private func logScreen(name: String, path: String) { private func logScreen(
name: String,
path: String,
params: [String: Any]?
) {
guard isEnabled else { return } guard isEnabled else { return }
managers.forEach { manager in managers.values.forEach { manager in
manager.logScreen(name: name, path: path) manager.logScreen(
name: name,
path: path,
params: params
)
} }
} }
@ -157,7 +231,7 @@ class AnalyticsManager {
) { ) {
guard isEnabled else { return } guard isEnabled else { return }
managers.forEach { manager in managers.values.forEach { manager in
manager.logEvent( manager.logEvent(
name: name, name: name,
action: action, action: action,
@ -169,28 +243,39 @@ class AnalyticsManager {
// MARK: - section_one // MARK: - section_one
func logScreenS1DefOne() { static func logScreenS1DefOne(title: String) {
logScreen( AnalyticsManager.shared.logScreen(
name: "s1 def one", name: "s1 def one \(title)",
path: "s1_def_one/" path: "s1_def_one/\(title)",
params: nil
) )
} }
func logEventS1DefTwo() { static func logEventS1DefTwo(
logEvent( title: String,
count: String,
test2: String = "test"
) {
AnalyticsManager.shared.logEvent(
name: "s1 def two", name: "s1 def two",
action: "test", action: "test",
category: "test", category: "test",
params: [] params: [
"title": title,
"count": count,
"test": "test",
"test2": test2
]
) )
} }
// MARK: - section_two // MARK: - section_two
func logScreenS2DefOne() { static func logScreenS2DefOne() {
logScreen( AnalyticsManager.shared.logScreen(
name: "s2 def one", name: "s2 def one",
path: "s2_def_one/" path: "s2_def_one/",
params: nil
) )
} }
} }

View File

@ -3,9 +3,13 @@ categories:
- id: section_one - id: section_one
screens: screens:
- id: s1_def_one - id: s1_def_one
name: s1 def one name: s1 def one _TITLE_
path: s1_def_one/ path: s1_def_one/_TITLE_
tags: ios tags: ios,droid
parameters:
- name: title
type: String
replaceIn: name,path
events: events:
- id: s1_def_two - id: s1_def_two
@ -13,6 +17,17 @@ categories:
action: test action: test
category: test category: test
tags: ios tags: ios
parameters:
- name: title
type: String
- name: count
type: String
- name: test
type: String
value: test
- name: test2
type: String
defaultValue: test
- id: section_two - id: section_two
screens: screens:

View File

@ -5,8 +5,8 @@ FORCE_FLAG="$1"
## Font ## Font
#swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/sampleFontsAll.txt" \ #swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/sampleFontsAll.txt" \
# --extension-output-path "./Fonts/Generated" \ # --extension-output-path "./Fonts/Generated" \
# --extension-name "UIFontYolo" \ # --extension-name "FontYolo" \
# --extension-name-swift-ui "FontYolo" \ # --extension-name-ui-kit "UIFontYolo" \
# --extension-suffix "GenAllScript" \ # --extension-suffix "GenAllScript" \
# --info-plist-paths "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist" # --info-plist-paths "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist"
# #
@ -17,8 +17,8 @@ FORCE_FLAG="$1"
# --style all \ # --style all \
# --xcassets-path "./Colors/colors.xcassets" \ # --xcassets-path "./Colors/colors.xcassets" \
# --extension-output-path "./Colors/Generated/" \ # --extension-output-path "./Colors/Generated/" \
# --extension-name "UIColorYolo" \ # --extension-name "ColorYolo" \
# --extension-name-swift-ui "ColorYolo" \ # --extension-name-ui-kit "UIhkjhkColorYolo" \
# --extension-suffix "GenAllScript" # --extension-suffix "GenAllScript"
# #
#echo "\n-------------------------\n" #echo "\n-------------------------\n"
@ -49,22 +49,23 @@ FORCE_FLAG="$1"
# --extension-output-path "./Tags/Generated" \ # --extension-output-path "./Tags/Generated" \
# --extension-name "Tags" \ # --extension-name "Tags" \
# --extension-suffix "GenAllScript" # --extension-suffix "GenAllScript"
#
echo "\n-------------------------\n" #echo "\n-------------------------\n"
# Analytics # Analytics
swift run -c release ResgenSwift analytics $FORCE_FLAG "./Tags/sampleTags.yml" \ swift run -c release ResgenSwift analytics $FORCE_FLAG "./Tags/sampleTags.yml" \
--target "matomo firebase" \ --target "firebase matomo" \
--extension-output-path "./Tags/Generated" \ --extension-output-path "./Tags/Generated" \
--extension-name "Analytics" \ --extension-name "Analytics" \
--extension-suffix "GenAllScript" --extension-suffix "GenAllScript" \
--static-members true
echo "\n-------------------------\n" #echo "\n-------------------------\n"
# #
## Images ## Images
#swift run -c release ResgenSwift images $FORCE_FLAG "./Images/sampleImages.txt" \ #swift run -c release ResgenSwift images $FORCE_FLAG "./Images/sampleImages.txt" \
# --xcassets-path "./Images/imagium.xcassets" \ # --xcassets-path "./Images/imagium.xcassets" \
# --extension-output-path "./Images/Generated" \ # --extension-output-path "./Images/Generated" \
# --extension-name "UIImage" \ # --extension-name "ImageYolo" \
# --extension-name-swift-ui "ImageYolo" \ # --extension-name-ui-kit "UIImageYolo" \
# --extension-suffix "GenAllScript" # --extension-suffix "GenAllScript"

View File

@ -47,8 +47,8 @@ images:
inputFile: ./Images/sampleImages.txt inputFile: ./Images/sampleImages.txt
xcassetsPath: ./Images/imagium.xcassets xcassetsPath: ./Images/imagium.xcassets
extensionOutputPath: ./Images/Generated extensionOutputPath: ./Images/Generated
extensionName: UIImage extensionName: ImageYolo
extensionNameSwiftUI: ImageYolo extensionNameUIKit: UIImageYolo
extensionSuffix: GenAllScript extensionSuffix: GenAllScript
@ -61,8 +61,8 @@ colors:
style: all style: all
xcassetsPath: ./Colors/colors.xcassets xcassetsPath: ./Colors/colors.xcassets
extensionOutputPath: ./Colors/Generated/ extensionOutputPath: ./Colors/Generated/
extensionName: UIColorYolo extensionName: ColorYolo
extensionNameSwiftUI: ColorYolo extensionNameUIKit: UIColorYolo
extensionSuffix: GenAllScript extensionSuffix: GenAllScript
@ -97,7 +97,7 @@ fonts:
- -
inputFile: ./Fonts/sampleFontsAll.txt inputFile: ./Fonts/sampleFontsAll.txt
extensionOutputPath: ./Fonts/Generated extensionOutputPath: ./Fonts/Generated
extensionName: UIFontYolo extensionName: FontYolo
extensionNameSwiftUI: FontYolo extensionNameUIKit: UIFontYolo
extensionSuffix: GenAllScript extensionSuffix: GenAllScript
infoPlistPaths: "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist" infoPlistPaths: "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist"

View File

@ -18,7 +18,6 @@ struct Analytics: ParsableCommand {
version: ResgenSwiftVersion version: ResgenSwiftVersion
) )
// MARK: - Static // MARK: - Static
static let toolName = "Analytics" static let toolName = "Analytics"
@ -33,8 +32,15 @@ struct Analytics: ParsableCommand {
mutating func run() { mutating func run() {
print("[\(Self.toolName)] Starting analytics generation") print("[\(Self.toolName)] Starting analytics generation")
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate analytics for target: \(options.target)") print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate analytics for target: \(options.target)")
// Check requirements
guard checkRequirements() else { return }
print("[\(Self.toolName)] Will generate analytics") print("[\(Self.toolName)] Will generate analytics")
// Check requirements
guard checkRequirements() else { return }
// Parse input file // Parse input file
let sections = AnalyticsFileParser.parse(options.inputFile, target: options.target) let sections = AnalyticsFileParser.parse(options.inputFile, target: options.target)
@ -48,20 +54,33 @@ struct Analytics: ParsableCommand {
print("[\(Self.toolName)] Analytics generated") print("[\(Self.toolName)] Analytics generated")
} }
}
extension Analytics { // MARK: - Requirements
enum TargetType: CaseIterable {
case matomo
case firebase
var value: String { private func checkRequirements() -> Bool {
switch self { let fileManager = FileManager()
case .matomo:
"matomo" // Input file
case .firebase: guard fileManager.fileExists(atPath: options.inputFile) else {
"firebase" let error = AnalyticsError.fileNotExists(options.inputFile)
print(error.description)
Analytics.exit(withError: error)
} }
guard TrackerType.hasValidTarget(in: options.target) else {
let error = AnalyticsError.noValidTracker(options.target)
print(error.description)
Analytics.exit(withError: error)
} }
// Check if needed to regenerate
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
print("[\(Self.toolName)] Analytics are already up to date :) ")
return false
}
return true
} }
} }

View File

@ -0,0 +1,39 @@
//
// AnalyticsError.swift
//
//
// Created by Loris Perret on 11/12/2023.
//
import Foundation
enum AnalyticsError: Error {
case noValidTracker(String)
case fileNotExists(String)
case missingElement(String)
case invalidParameter(String)
case parseFailed(String)
case writeFile(String, String)
var description: String {
switch self {
case .noValidTracker(let inputTargets):
return "error: [\(Analytics.toolName)] '\(inputTargets)' ne contient aucun tracker valid"
case .fileNotExists(let filename):
return "error: [\(Analytics.toolName)] File \(filename) does not exists"
case .missingElement(let element):
return "error: [\(Analytics.toolName)] Missing \(element) for Matomo"
case .invalidParameter(let reason):
return "error: [\(Analytics.toolName)] Invalid parameter \(reason)"
case .parseFailed(let baseError):
return "error: [\(Analytics.toolName)] Parse input file failed: \(baseError)"
case .writeFile(let subErrorDescription, let filename):
return "error: [\(Analytics.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
}
}
}

View File

@ -10,13 +10,13 @@ import ToolCore
import CoreVideo import CoreVideo
class AnalyticsGenerator { class AnalyticsGenerator {
static var targets: [Analytics.TargetType] = [] static var targets: [TrackerType] = []
static func writeExtensionFiles(sections: [AnalyticsCategory], target: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) { static func writeExtensionFiles(sections: [AnalyticsCategory], target: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
// Get target type from enum // Get target type from enum
let targetsString: [String] = target.components(separatedBy: " ") let targetsString: [String] = target.components(separatedBy: " ")
Analytics.TargetType.allCases.forEach { enumTarget in TrackerType.allCases.forEach { enumTarget in
if targetsString.contains(enumTarget.value) { if targetsString.contains(enumTarget.value) {
targets.append(enumTarget) targets.append(enumTarget)
} }
@ -32,10 +32,10 @@ class AnalyticsGenerator {
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do { do {
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) let error = AnalyticsError.writeFile(extensionFilePath, error.localizedDescription)
print(error.description) print(error.description)
Stringium.exit(withError: error) Analytics.exit(withError: error)
} }
} }
@ -58,15 +58,19 @@ class AnalyticsGenerator {
\(Self.getImport()) \(Self.getImport())
\(Self.getAnalytics()) \(Self.getAnalyticsProtocol())
\(Self.getTrackerTypeEnum())
// MARK: - Manager // MARK: - Manager
class AnalyticsManager { class AnalyticsManager {
static var shared = AnalyticsManager() static var shared = AnalyticsManager()
// MARK: - Properties // MARK: - Properties
var managers: [AnalyticsManagerProtocol] = [] var managers: [TrackerType: AnalyticsManagerProtocol] = [:]
\(Self.getEnabledContent()) \(Self.getEnabledContent())
@ -76,14 +80,49 @@ class AnalyticsGenerator {
""" """
} }
private static func getTrackerTypeEnum() -> String {
var result: [String] = []
TrackerType.allCases.forEach { type in
result.append(" case \(type)")
}
return """
// MARK: - Traker Type
enum TrackerType: CaseIterable {
\(result.joined(separator: "\n"))
}
"""
}
private static func getEnabledContent() -> String { private static func getEnabledContent() -> String {
""" """
private var isEnabled: Bool = true private var isEnabled: Bool {
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
false
} else {
true
}
}
// MARK: - Methods // MARK: - Methods
func setAnalyticsEnabled(_ enable: Bool) { private func setAnalytics(enable: Bool, _ analytics: [TrackerType]) {
isEnabled = enable managers.forEach { (key, value) in
if analytics.contains(where: { type in
type == key
}) {
value.setEnable(enable)
}
}
}
func enableAnalytics(_ analytics: [TrackerType] = TrackerType.allCases) {
setAnalytics(enable: true, analytics)
}
func disableAnalytics(_ analytics: [TrackerType] = TrackerType.allCases) {
setAnalytics(enable: false, analytics)
} }
""" """
} }
@ -91,10 +130,10 @@ class AnalyticsGenerator {
private static func getImport() -> String { private static func getImport() -> String {
var result: [String] = [] var result: [String] = []
if targets.contains(Analytics.TargetType.matomo) { if targets.contains(TrackerType.matomo) {
result.append("import MatomoTracker") result.append("import MatomoTracker")
} }
if targets.contains(Analytics.TargetType.firebase) { if targets.contains(TrackerType.firebase) {
result.append("import FirebaseAnalytics") result.append("import FirebaseAnalytics")
} }
@ -103,11 +142,19 @@ class AnalyticsGenerator {
private static func getPrivateLogFunction() -> String { private static func getPrivateLogFunction() -> String {
""" """
private func logScreen(name: String, path: String) { private func logScreen(
name: String,
path: String,
params: [String: Any]?
) {
guard isEnabled else { return } guard isEnabled else { return }
managers.forEach { manager in managers.values.forEach { manager in
manager.logScreen(name: name, path: path) manager.logScreen(
name: name,
path: path,
params: params
)
} }
} }
@ -119,7 +166,7 @@ class AnalyticsGenerator {
) { ) {
guard isEnabled else { return } guard isEnabled else { return }
managers.forEach { manager in managers.values.forEach { manager in
manager.logEvent( manager.logEvent(
name: name, name: name,
action: action, action: action,
@ -136,24 +183,22 @@ class AnalyticsGenerator {
var content: [String] = [] var content: [String] = []
let footer = " }" let footer = " }"
if targets.contains(Analytics.TargetType.matomo) { if targets.contains(TrackerType.matomo) {
header = "func configure(siteId: String, url: String) {" header = "func configure(siteId: String, url: String) {"
} else if targets.contains(Analytics.TargetType.firebase) { } else if targets.contains(TrackerType.firebase) {
header = "func configure() {" header = "func configure() {"
} }
if targets.contains(Analytics.TargetType.matomo) { if targets.contains(TrackerType.matomo) {
content.append(""" content.append("""
managers.append( managers[TrackerType.matomo] = MatomoAnalyticsManager(
MatomoAnalyticsManager(
siteId: siteId, siteId: siteId,
url: url url: url
) )
)
""") """)
} }
if targets.contains(Analytics.TargetType.firebase) { if targets.contains(TrackerType.firebase) {
content.append(" managers.append(FirebaseAnalyticsManager())") content.append(" managers[TrackerType.firebase] = FirebaseAnalyticsManager()")
} }
return [ return [
@ -164,30 +209,37 @@ class AnalyticsGenerator {
.joined(separator: "\n") .joined(separator: "\n")
} }
private static func getAnalytics() -> String { private static func getAnalyticsProtocol() -> String {
let proto = """ let proto = """
// MARK: - Protocol // MARK: - Protocol
protocol AnalyticsManagerProtocol { protocol AnalyticsManagerProtocol {
func logScreen(name: String, path: String) func logScreen(
name: String,
path: String,
params: [String: Any]?
)
func logEvent( func logEvent(
name: String, name: String,
action: String, action: String,
category: String, category: String,
params: [String: Any]? params: [String: Any]?
) )
func setEnable(_ enable: Bool)
} }
""" """
var result: [String] = [proto] var result: [String] = [proto]
if targets.contains(Analytics.TargetType.matomo) { if targets.contains(TrackerType.matomo) {
result.append(MatomoGenerator.service.content) result.append(MatomoGenerator.service)
} }
if targets.contains(Analytics.TargetType.firebase) { if targets.contains(TrackerType.firebase) {
result.append(FirebaseGenerator.service.content) result.append(FirebaseGenerator.service)
} }
return result.joined(separator: "\n") return result.joined(separator: "\n")

View File

@ -8,19 +8,21 @@
import Foundation import Foundation
enum FirebaseGenerator { enum FirebaseGenerator {
case service
var content: String { static var service: String {
[ [
FirebaseGenerator.service.header, FirebaseGenerator.header,
FirebaseGenerator.service.logScreen, FirebaseGenerator.logScreen,
FirebaseGenerator.service.logEvent, FirebaseGenerator.logEvent,
FirebaseGenerator.service.footer FirebaseGenerator.enable,
FirebaseGenerator.footer
] ]
.joined(separator: "\n") .joined(separator: "\n")
} }
private var header: String { // MARK: - Private vars
private static var header: String {
""" """
// MARK: - Firebase // MARK: - Firebase
@ -28,13 +30,33 @@ enum FirebaseGenerator {
""" """
} }
private var logScreen: String { private static var logScreen: String {
""" """
func logScreen(name: String, path: String) { func logScreen(
name: String,
path: String,
params: [String: Any]?
) {
var parameters = [ var parameters = [
AnalyticsParameterScreenName: name AnalyticsParameterScreenName: name as NSObject
] ]
if path.isEmpty == false {
parameters["path"] = path + "/iOS" as NSObject
}
if let supplementaryParameters = params {
for (newKey, newValue) in supplementaryParameters {
if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
}
}
Analytics.logEvent( Analytics.logEvent(
AnalyticsEventScreenView, AnalyticsEventScreenView,
parameters: parameters parameters: parameters
@ -44,7 +66,7 @@ enum FirebaseGenerator {
""" """
} }
private var logEvent: String { private static var logEvent: String {
""" """
func logEvent( func logEvent(
name: String, name: String,
@ -52,26 +74,47 @@ enum FirebaseGenerator {
category: String, category: String,
params: [String: Any]? params: [String: Any]?
) { ) {
var parameters: [String:Any] = [ var parameters: [String:NSObject] = [
"action": action, AnalyticsParameterItemName: name.replacingOccurrences(of: " ", with: "_") as NSObject
"category": category,
] ]
if category.isEmpty == false {
parameters["AnalyticsParameterItemCategory"] = category as NSObject
}
if action.isEmpty == false {
parameters["action"] = action as NSObject
}
if let supplementaryParameters = params { if let supplementaryParameters = params {
parameters.merge(supplementaryParameters) { (origin, new) -> Any in for (newKey, newValue) in supplementaryParameters {
return origin if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
} }
} }
Analytics.logEvent( Analytics.logEvent(
name, AnalyticsEventSelectContent,
parameters: parameters parameters: parameters
) )
} }
""" """
} }
private var footer: String { private static var enable: String {
"""
func setEnable(_ enable: Bool) {
Analytics.setAnalyticsCollectionEnabled(enable)
}
"""
}
private static var footer: String {
""" """
} }

View File

@ -8,20 +8,22 @@
import Foundation import Foundation
enum MatomoGenerator { enum MatomoGenerator {
case service
var content: String { static var service: String {
[ [
MatomoGenerator.service.header, MatomoGenerator.header,
MatomoGenerator.service.setup, MatomoGenerator.setup,
MatomoGenerator.service.logScreen, MatomoGenerator.logScreen,
MatomoGenerator.service.logEvent, MatomoGenerator.logEvent,
MatomoGenerator.service.footer MatomoGenerator.enable,
MatomoGenerator.footer
] ]
.joined(separator: "\n") .joined(separator: "\n")
} }
private var header: String { // MARK: - Private vars
private static var header: String {
""" """
// MARK: - Matomo // MARK: - Matomo
@ -34,7 +36,7 @@ enum MatomoGenerator {
""" """
} }
private var setup: String { private static var setup: String {
""" """
// MARK: - Init // MARK: - Init
@ -63,10 +65,13 @@ enum MatomoGenerator {
""" """
} }
private var logScreen: String { private static var logScreen: String {
""" """
func logScreen(name: String, path: String) { func logScreen(
guard !tracker.isOptedOut else { return } name: String,
path: String,
params: [String: Any]?
) {
guard let trackerUrl = tracker.contentBase?.absoluteString else { return } guard let trackerUrl = tracker.contentBase?.absoluteString else { return }
let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS") let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS")
@ -79,7 +84,7 @@ enum MatomoGenerator {
""" """
} }
private var logEvent: String { private static var logEvent: String {
""" """
func logEvent( func logEvent(
name: String, name: String,
@ -87,8 +92,6 @@ enum MatomoGenerator {
category: String, category: String,
params: [String: Any]? params: [String: Any]?
) { ) {
guard !tracker.isOptedOut else { return }
tracker.track( tracker.track(
eventWithCategory: category, eventWithCategory: category,
action: action, action: action,
@ -97,10 +100,19 @@ enum MatomoGenerator {
url: nil url: nil
) )
} }
""" """
} }
private var footer: String { private static var enable: String {
"""
func setEnable(_ enable: Bool) {
tracker.isOptedOut = !enable
}
"""
}
private static var footer: String {
""" """
} }

View File

@ -11,10 +11,14 @@ class AnalyticsCategory {
let id: String // OnBoarding let id: String // OnBoarding
var definitions = [AnalyticsDefinition]() var definitions = [AnalyticsDefinition]()
// MARK: - Init
init(id: String) { init(id: String) {
self.id = id self.id = id
} }
// MARK: - Methods
func hasOneOrMoreMatchingTags(tags: [String]) -> Bool { func hasOneOrMoreMatchingTags(tags: [String]) -> Bool {
let allTags = definitions.flatMap { $0.tags } let allTags = definitions.flatMap { $0.tags }
let allTagsSet = Set(allTags) let allTagsSet = Set(allTags)

View File

@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import ToolCore
class AnalyticsDefinition { class AnalyticsDefinition {
let id: String let id: String
@ -18,20 +19,24 @@ class AnalyticsDefinition {
var parameters: [AnalyticsParameter] = [] var parameters: [AnalyticsParameter] = []
var type: TagType var type: TagType
// MARK: - Init
init(id: String, name: String, type: TagType) { init(id: String, name: String, type: TagType) {
self.id = id self.id = id
self.name = name self.name = name
self.type = type self.type = type
} }
// MARK: - Methods
func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool { func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool {
if Set(inputTags).intersection(Set(self.tags)).isEmpty { if Set(inputTags).isDisjoint(with: tags) {
return false return false
} }
return true return true
} }
// MARK: - Methods // MARK: - Private Methods
private func getFuncName() -> String { private func getFuncName() -> String {
var pascalCaseTitle: String = "" var pascalCaseTitle: String = ""
@ -43,17 +48,23 @@ class AnalyticsDefinition {
} }
private func getParameters() -> String { private func getParameters() -> String {
var params = parameters
var result: String var result: String
if type == .screen { let paramsString = parameters.compactMap { parameter -> String? in
params = params.filter{ param in guard parameter.value.isEmpty else { return nil }
!param.replaceIn.isEmpty
} let defaultValue: String
switch parameter.type {
case .bool:
defaultValue = "\(parameter.defaultValue.lowercased())"
case .int, .double:
defaultValue = "\(parameter.defaultValue)"
case .string:
defaultValue = "\"\(parameter.defaultValue)\""
} }
let paramsString = params.map { parameter in let defaultValueString = parameter.defaultValue.isEmpty ? "" : " = \(defaultValue)"
"\(parameter.name): \(parameter.type)" return "\(parameter.name): \(parameter.type.rawValue)\(defaultValueString)"
} }
if paramsString.count > 2 { if paramsString.count > 2 {
@ -71,7 +82,7 @@ class AnalyticsDefinition {
return result return result
} }
private func replaceIn(){ private func replaceIn() {
for parameter in parameters { for parameter in parameters {
for rep in parameter.replaceIn { for rep in parameter.replaceIn {
switch rep { switch rep {
@ -79,7 +90,10 @@ class AnalyticsDefinition {
case "path": path = path.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))") case "path": path = path.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
case "category": category = category.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))") case "category": category = category.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
case "action": action = action.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))") case "action": action = action.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
default: break default:
if let param = parameters.first(where: { $0.name == rep }), param.value.isEmpty == false {
param.value = param.value.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
}
} }
} }
} }
@ -94,7 +108,18 @@ class AnalyticsDefinition {
} }
supplementaryParams.forEach { param in supplementaryParams.forEach { param in
if param.value.isEmpty {
params.append("\"\(param.name)\": \(param.name)") params.append("\"\(param.name)\": \(param.name)")
} else {
switch param.type {
case .bool:
params.append("\"\(param.name)\": \(param.value.lowercased())")
case .int, .double:
params.append("\"\(param.name)\": \(param.value)")
case .string:
params.append("\"\(param.name)\": \"\(param.value)\"")
}
}
} }
if params.count > 1 { if params.count > 1 {
@ -108,14 +133,15 @@ class AnalyticsDefinition {
[\(params.joined(separator: ", "))] [\(params.joined(separator: ", "))]
""" """
} else { } else {
result = "[:]" result = "nil"
} }
if type == .screen { if type == .screen {
return """ return """
logScreen( logScreen(
name: "\(name)", name: "\(name)",
path: "\(path)" path: "\(path)",
params: \(result)
) )
""" """
} else { } else {
@ -145,32 +171,8 @@ class AnalyticsDefinition {
replaceIn() replaceIn()
return """ return """
static func \(getFuncName())\(getParameters()) { static func \(getFuncName())\(getParameters()) {
\(getlogFunction()) AnalyticsManager.shared.\(getlogFunction())
} }
""" """
} }
} }
extension AnalyticsDefinition {
enum TagType {
case screen
case event
}
}
extension String {
func replacingFirstOccurrence(of: String, with: String) -> Self {
if let range = self.range(of: of) {
let tmp = self.replacingOccurrences(
of: of,
with: with,
options: .literal,
range: range
)
return tmp
}
return self
}
}

View File

@ -17,9 +17,7 @@ struct AnalyticsCategoryDTO: Codable {
var events: [AnalyticsDefinitionEventDTO]? var events: [AnalyticsDefinitionEventDTO]?
} }
protocol AnalyticsDefinitionDTO: Codable {} struct AnalyticsDefinitionScreenDTO: Codable {
struct AnalyticsDefinitionScreenDTO: AnalyticsDefinitionDTO {
var id: String var id: String
var name: String var name: String
var tags: String var tags: String
@ -29,7 +27,7 @@ struct AnalyticsDefinitionScreenDTO: AnalyticsDefinitionDTO {
var path: String? var path: String?
} }
struct AnalyticsDefinitionEventDTO: AnalyticsDefinitionDTO { struct AnalyticsDefinitionEventDTO: Codable {
var id: String var id: String
var name: String var name: String
var tags: String var tags: String
@ -43,5 +41,7 @@ struct AnalyticsDefinitionEventDTO: AnalyticsDefinitionDTO {
struct AnalyticsParameterDTO: Codable { struct AnalyticsParameterDTO: Codable {
var name: String var name: String
var type: String var type: String
var value: String?
var defaultValue: String?
var replaceIn: String? var replaceIn: String?
} }

View File

@ -9,11 +9,17 @@ import Foundation
class AnalyticsParameter { class AnalyticsParameter {
var name: String var name: String
var type: String var type: ParameterType
var value: String
var defaultValue: String
var replaceIn: [String] = [] var replaceIn: [String] = []
init(name: String, type: String) { // MARK: - Init
init(name: String, type: ParameterType, value: String, defaultValue: String) {
self.name = name self.name = name
self.type = type self.type = type
self.value = value
self.defaultValue = defaultValue
} }
} }

View File

@ -0,0 +1,15 @@
//
// File.swift
//
//
// Created by Loris Perret on 17/07/2024.
//
import Foundation
enum ParameterType: String {
case string = "String"
case int = "Int"
case double = "Double"
case bool = "Bool"
}

View File

@ -0,0 +1,16 @@
//
// TagType.swift
//
//
// Created by Thibaut Schmitt on 08/12/2023.
//
import Foundation
extension AnalyticsDefinition {
enum TagType {
case screen
case event
}
}

View File

@ -0,0 +1,29 @@
//
// TargetType.swift
//
//
// Created by Thibaut Schmitt on 08/12/2023.
//
import Foundation
enum TrackerType: CaseIterable {
case matomo
case firebase
var value: String {
switch self {
case .matomo:
"matomo"
case .firebase:
"firebase"
}
}
static func hasValidTarget(in targets: String) -> Bool {
for tracker in Self.allCases where targets.contains(tracker.value) {
return true
}
return false
}
}

View File

@ -14,46 +14,82 @@ class AnalyticsFileParser {
private static func parseYaml() -> AnalyticsFile { private static func parseYaml() -> AnalyticsFile {
guard let data = FileManager().contents(atPath: inputFile) else { guard let data = FileManager().contents(atPath: inputFile) else {
let error = GenerateError.fileNotExists(inputFile) let error = AnalyticsError.fileNotExists(inputFile)
Generate.exit(withError: error) print(error.description)
Analytics.exit(withError: error)
} }
do { do {
let tagFile = try YAMLDecoder().decode(AnalyticsFile.self, from: data) let tagFile = try YAMLDecoder().decode(AnalyticsFile.self, from: data)
return tagFile return tagFile
} catch let error { } catch {
Generate.exit(withError: error) let error = AnalyticsError.parseFailed(error.localizedDescription)
print(error.description)
Analytics.exit(withError: error)
} }
} }
private static func getParameters(fromData data: [AnalyticsParameterDTO]) -> [AnalyticsParameter] { private static func getParameters(from parameters: [AnalyticsParameterDTO]) -> [AnalyticsParameter] {
var parameters: [AnalyticsParameter] = [] func verify(value: String?, for type: ParameterType) {
guard let value, value.isEmpty == false else { return }
data.forEach { value in switch type {
case .int:
if Int(value) == nil {
let error = AnalyticsError.invalidParameter("type of \(value) is not \(type)")
print(error.description)
Analytics.exit(withError: error)
}
case .bool:
if Bool(value.lowercased()) == nil {
let error = AnalyticsError.invalidParameter("type of \(value) is not \(type)")
print(error.description)
Analytics.exit(withError: error)
}
case .double:
if Double(value) == nil {
let error = AnalyticsError.invalidParameter("type of \(value) is not \(type)")
print(error.description)
Analytics.exit(withError: error)
}
case .string:
break
}
}
return parameters.map { dtoParameter in
// Type // Type
let type = value.type.uppercasedFirst() let type = dtoParameter.type.uppercasedFirst()
guard guard let typeEnum = ParameterType(rawValue: type) else {
type == "String" || let error = AnalyticsError.invalidParameter("type of \(dtoParameter.name)")
type == "Int" || print(error.description)
type == "Double" || Analytics.exit(withError: error)
type == "Bool"
else {
let error = GenerateError.invalidParameter("type of \(value.name)")
Generate.exit(withError: error)
} }
let parameter: AnalyticsParameter = AnalyticsParameter(name: value.name, type: type) if dtoParameter.value != nil, dtoParameter.replaceIn != nil {
let error = AnalyticsError.invalidParameter("you can't set 'value' and 'replaceIn' for \(dtoParameter.name)")
print(error.description)
Analytics.exit(withError: error)
}
if let replaceIn = value.replaceIn { verify(value: dtoParameter.value, for: typeEnum)
verify(value: dtoParameter.defaultValue, for: typeEnum)
let parameter = AnalyticsParameter(
name: dtoParameter.name,
type: typeEnum,
value: dtoParameter.value ?? "",
defaultValue: dtoParameter.defaultValue ?? ""
)
if let replaceIn = dtoParameter.replaceIn {
parameter.replaceIn = replaceIn.components(separatedBy: ",") parameter.replaceIn = replaceIn.components(separatedBy: ",")
} }
parameters.append(parameter) return parameter
} }
return parameters
} }
private static func getTagDefinition( private static func getTagDefinition(
@ -64,24 +100,24 @@ class AnalyticsFileParser {
comments: String?, comments: String?,
parameters: [AnalyticsParameterDTO]? parameters: [AnalyticsParameterDTO]?
) -> AnalyticsDefinition { ) -> AnalyticsDefinition {
let definition: AnalyticsDefinition = AnalyticsDefinition(id: id, name: name, type: type) let definition = AnalyticsDefinition(id: id, name: name, type: type)
definition.tags = tags.components(separatedBy: ",") definition.tags = tags
.components(separatedBy: ",")
.map { $0.removeLeadingTrailingWhitespace() }
if let comments = comments { if let comments = comments {
definition.comments = comments definition.comments = comments
} }
if let parameters = parameters { if let parameters = parameters {
definition.parameters = Self.getParameters(fromData: parameters) definition.parameters = Self.getParameters(from: parameters)
} }
return definition return definition
} }
private static func getTagDefinitionScreen(fromData screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] { private static func getTagDefinitionScreen(from screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] {
var definitions: [AnalyticsDefinition] = [] screens.map { screen in
for screen in screens {
let definition: AnalyticsDefinition = Self.getTagDefinition( let definition: AnalyticsDefinition = Self.getTagDefinition(
id: screen.id, id: screen.id,
name: screen.name, name: screen.name,
@ -91,27 +127,26 @@ class AnalyticsFileParser {
parameters: screen.parameters parameters: screen.parameters
) )
if target.contains(Analytics.TargetType.matomo.value) { if target.contains(TrackerType.matomo.value) {
// Path // Path
guard let path = screen.path else { guard let path = screen.path else {
let error = GenerateError.missingElement("screen path") let error = AnalyticsError.missingElement("screen path")
Generate.exit(withError: error) print(error.description)
Analytics.exit(withError: error)
} }
definition.path = path
} else if let path = screen.path {
definition.path = path definition.path = path
} }
definitions.append(definition) return definition
}
} }
return definitions private static func getTagDefinitionEvent(from events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] {
} events.map { event in
private static func getTagDefinitionEvent(fromData events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] {
var definitions: [AnalyticsDefinition] = []
for event in events {
let definition: AnalyticsDefinition = Self.getTagDefinition( let definition: AnalyticsDefinition = Self.getTagDefinition(
id: event.id, id: event.id,
name: event.name, name: event.name,
@ -121,53 +156,66 @@ class AnalyticsFileParser {
parameters: event.parameters parameters: event.parameters
) )
if target.contains(Analytics.TargetType.matomo.value) { if target.contains(TrackerType.matomo.value) {
// Category // Category
guard let category = event.category else { guard let category = event.category else {
let error = GenerateError.missingElement("event category") let error = AnalyticsError.missingElement("event category")
Generate.exit(withError: error) print(error.description)
Analytics.exit(withError: error)
} }
definition.category = category definition.category = category
// Action // Action
guard let action = event.action else { guard let action = event.action else {
let error = GenerateError.missingElement("event action") let error = AnalyticsError.missingElement("event action")
Generate.exit(withError: error) print(error.description)
Analytics.exit(withError: error)
} }
definition.action = action definition.action = action
} else {
if let category = event.category {
definition.category = category
} }
definitions.append(definition) if let action = event.action {
definition.action = action
}
} }
return definitions return definition
}
} }
static func parse(_ inputFile: String, target: String) -> [AnalyticsCategory] { static func parse(_ inputFile: String, target: String) -> [AnalyticsCategory] {
self.inputFile = inputFile self.inputFile = inputFile
self.target = target self.target = target
let tagFile: AnalyticsFile = Self.parseYaml() let tagFile = Self.parseYaml()
var sections: [AnalyticsCategory] = []
tagFile.categories.forEach { categorie in return tagFile
.categories
.map { categorie in
let section: AnalyticsCategory = AnalyticsCategory(id: categorie.id) let section: AnalyticsCategory = AnalyticsCategory(id: categorie.id)
if let screens = categorie.screens { if let screens = categorie.screens {
section.definitions.append(contentsOf: Self.getTagDefinitionScreen(fromData: screens)) section
.definitions
.append(
contentsOf: Self.getTagDefinitionScreen(from: screens)
)
} }
if let events = categorie.events { if let events = categorie.events {
section.definitions.append(contentsOf: Self.getTagDefinitionEvent(fromData: events)) section
.definitions
.append(
contentsOf: Self.getTagDefinitionEvent(from: events)
)
} }
sections.append(section) return section
} }
return sections
} }
} }

View File

@ -21,8 +21,8 @@ struct Colors: ParsableCommand {
// MARK: - Static // MARK: - Static
static let toolName = "Color" static let toolName = "Color"
static let defaultExtensionName = "UIColor" static let defaultExtensionName = "Color"
static let defaultExtensionNameSUI = "Color" static let defaultExtensionNameUIKit = "UIColor"
static let assetsColorsFolderName = "Colors" static let assetsColorsFolderName = "Colors"
// MARK: - Command options // MARK: - Command options
@ -57,14 +57,14 @@ struct Colors: ParsableCommand {
staticVar: options.staticMembers, staticVar: options.staticMembers,
extensionName: options.extensionName, extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath, extensionFilePath: options.extensionFilePath,
isSwiftUI: false) isSwiftUI: true)
// Generate extension // Generate extension
ColorExtensionGenerator.writeExtensionFile(colors: parsedColors, ColorExtensionGenerator.writeExtensionFile(colors: parsedColors,
staticVar: options.staticMembers, staticVar: options.staticMembers,
extensionName: options.extensionNameSwiftUI, extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathSwiftUI, extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: true) isSwiftUI: false)
print("[\(Self.toolName)] Colors generated") print("[\(Self.toolName)] Colors generated")
} }
@ -89,7 +89,7 @@ struct Colors: ParsableCommand {
} }
// Extension for UIKit and SwiftUI should have different name // Extension for UIKit and SwiftUI should have different name
guard options.extensionName != options.extensionNameSwiftUI else { guard options.extensionName != options.extensionNameUIKit else {
let error = ColorsToolError.extensionNamesCollision(options.extensionName) let error = ColorsToolError.extensionNamesCollision(options.extensionName)
print(error.description) print(error.description)
Colors.exit(withError: error) Colors.exit(withError: error)

View File

@ -27,11 +27,11 @@ struct ColorsToolOptions: ParsableArguments {
@Option(help: "Tell if it will generate static properties or not") @Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate an UIColor extension.") @Option(help: "Extension name. If not specified, it will generate an Color extension.")
var extensionName: String = Colors.defaultExtensionName var extensionName: String = Colors.defaultExtensionName
@Option(help: "SwiftUI Extension name. If not specified, it will generate an Color extension.") @Option(help: "SwiftUI Extension name. If not specified, it will generate an UIColor extension.")
var extensionNameSwiftUI: String = Colors.defaultExtensionNameSUI var extensionNameUIKit: String = Colors.defaultExtensionNameUIKit
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift") @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift")
var extensionSuffix: String? var extensionSuffix: String?
@ -41,7 +41,7 @@ struct ColorsToolOptions: ParsableArguments {
extension ColorsToolOptions { extension ColorsToolOptions {
// MARK: - UIKit // MARK: - SwiftUI
var extensionFileName: String { var extensionFileName: String {
if let extensionSuffix = extensionSuffix { if let extensionSuffix = extensionSuffix {
@ -54,16 +54,16 @@ extension ColorsToolOptions {
"\(extensionOutputPath)/\(extensionFileName)" "\(extensionOutputPath)/\(extensionFileName)"
} }
// MARK: - SwiftUI // MARK: - UIKit
var extensionFileNameSwiftUI: String { var extensionFileNameUIKit: String {
if let extensionSuffix = extensionSuffix { if let extensionSuffix = extensionSuffix {
return "\(extensionNameSwiftUI)+\(extensionSuffix).swift" return "\(extensionNameUIKit)+\(extensionSuffix).swift"
} }
return "\(extensionNameSwiftUI).swift" return "\(extensionNameUIKit).swift"
} }
var extensionFilePathSwiftUI: String { var extensionFilePathUIKit: String {
"\(extensionOutputPath)/\(extensionFileNameSwiftUI)" "\(extensionOutputPath)/\(extensionFileNameUIKit)"
} }
} }

View File

@ -30,7 +30,7 @@ struct ColorExtensionGenerator {
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do { do {
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = ColorsToolError.writeExtension(extensionFilePath, error.localizedDescription) let error = ColorsToolError.writeExtension(extensionFilePath, error.localizedDescription)
print(error.description) print(error.description)
Colors.exit(withError: error) Colors.exit(withError: error)

View File

@ -38,7 +38,7 @@ struct ColorXcassetHelper {
let contentsJsonPathURL = URL(fileURLWithPath: contentsJsonPath) let contentsJsonPathURL = URL(fileURLWithPath: contentsJsonPath)
do { do {
try color.contentsJSON().write(to: contentsJsonPathURL, atomically: false, encoding: .utf8) try color.contentsJSON().write(to: contentsJsonPathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = ColorsToolError.writeAsset(error.localizedDescription) let error = ColorsToolError.writeAsset(error.localizedDescription)
print(error.description) print(error.description)
Colors.exit(withError: error) Colors.exit(withError: error)

View File

@ -21,11 +21,11 @@ struct FontsOptions: ParsableArguments {
@Option(help: "Tell if it will generate static properties or methods") @Option(help: "Tell if it will generate static properties or methods")
var staticMembers: Bool = false var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate an UIFont extension.") @Option(help: "Extension name. If not specified, it will generate an Font extension.")
var extensionName: String = Fonts.defaultExtensionName var extensionName: String = Fonts.defaultExtensionName
@Option(help: "Extension name. If not specified, it will generate an Font extension.") @Option(help: "Extension name. If not specified, it will generate an UIFont extension.")
var extensionNameSwiftUI: String = Fonts.defaultExtensionNameSUI var extensionNameUIKit: String = Fonts.defaultExtensionNameUIKit
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+FontsMyApp.swift") @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+FontsMyApp.swift")
var extensionSuffix: String = "" var extensionSuffix: String = ""
@ -38,7 +38,7 @@ struct FontsOptions: ParsableArguments {
extension FontsOptions { extension FontsOptions {
// MARK: - UIKit // MARK: - SwiftUI
var extensionFileName: String { var extensionFileName: String {
if extensionSuffix.isEmpty == false { if extensionSuffix.isEmpty == false {
@ -51,17 +51,17 @@ extension FontsOptions {
"\(extensionOutputPath)/\(extensionFileName)" "\(extensionOutputPath)/\(extensionFileName)"
} }
// MARK: - SwiftUI // MARK: - UIKit
var extensionFileNameSwiftUI: String { var extensionFileNameUIKit: String {
if extensionSuffix.isEmpty == false { if extensionSuffix.isEmpty == false {
return "\(extensionNameSwiftUI)+\(extensionSuffix).swift" return "\(extensionNameUIKit)+\(extensionSuffix).swift"
} }
return "\(extensionNameSwiftUI).swift" return "\(extensionNameUIKit).swift"
} }
var extensionFilePathSwiftUI: String { var extensionFilePathUIKit: String {
"\(extensionOutputPath)/\(extensionFileNameSwiftUI)" "\(extensionOutputPath)/\(extensionFileNameUIKit)"
} }
// MARK: - // MARK: -

View File

@ -21,8 +21,8 @@ struct Fonts: ParsableCommand {
// MARK: - Static // MARK: - Static
static let toolName = "Fonts" static let toolName = "Fonts"
static let defaultExtensionName = "UIFont" static let defaultExtensionName = "Font"
static let defaultExtensionNameSUI = "Font" static let defaultExtensionNameUIKit = "UIFont"
// MARK: - Command Options // MARK: - Command Options
@ -52,13 +52,13 @@ struct Fonts: ParsableCommand {
staticVar: options.staticMembers, staticVar: options.staticMembers,
extensionName: options.extensionName, extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath, extensionFilePath: options.extensionFilePath,
isSwiftUI: false) isSwiftUI: true)
FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames, FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames,
staticVar: options.staticMembers, staticVar: options.staticMembers,
extensionName: options.extensionNameSwiftUI, extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathSwiftUI, extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: true) isSwiftUI: false)
print("Info.plist has been updated with:") print("Info.plist has been updated with:")
print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames, infoPlistPaths: options.infoPlistPaths))") print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames, infoPlistPaths: options.infoPlistPaths))")
@ -79,7 +79,7 @@ struct Fonts: ParsableCommand {
} }
// Extension for UIKit and SwiftUI should have different name // Extension for UIKit and SwiftUI should have different name
guard options.extensionName != options.extensionNameSwiftUI else { guard options.extensionName != options.extensionNameUIKit else {
let error = FontsToolError.extensionNamesCollision(options.extensionName) let error = FontsToolError.extensionNamesCollision(options.extensionName)
print(error.description) print(error.description)
Fonts.exit(withError: error) Fonts.exit(withError: error)

View File

@ -36,7 +36,7 @@ class FontExtensionGenerator {
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do { do {
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription) let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription)
print(error.description) print(error.description)
Fonts.exit(withError: error) Fonts.exit(withError: error)

View File

@ -9,22 +9,17 @@ import Foundation
enum GenerateError: Error { enum GenerateError: Error {
case fileNotExists(String) case fileNotExists(String)
case invalidConfigurationFile(String) case invalidConfigurationFile(String, String)
case commandError([String], String) case commandError([String], String)
case writeFile(String, String) case writeFile(String, String)
// Analytics
case missingElement(String)
case invalidParameter(String)
var description: String { var description: String {
switch self { switch self {
case .fileNotExists(let filename): case .fileNotExists(let filename):
return "error: [\(Generate.toolName)] File \(filename) does not exists" return "error: [\(Generate.toolName)] File \(filename) does not exists"
case .invalidConfigurationFile(let filename): case .invalidConfigurationFile(let filename, let underneathErrorDescription):
return "error: [\(Generate.toolName)] File \(filename) is not a valid configuration file" return "error: [\(Generate.toolName)] File \(filename) is not a valid configuration file. Underneath error: \(underneathErrorDescription)"
case .commandError(let command, let terminationStatus): case .commandError(let command, let terminationStatus):
let readableCommand = command let readableCommand = command
@ -34,12 +29,6 @@ enum GenerateError: Error {
case .writeFile(let filename, let info): case .writeFile(let filename, let info):
return "error: [\(Generate.toolName)] An error occured while writing file in \(filename): \(info)" return "error: [\(Generate.toolName)] An error occured while writing file in \(filename): \(info)"
case .missingElement(let element):
return "error: [\(Generate.toolName)] Missing \(element) for Matomo"
case .invalidParameter(let reason):
return "error: [\(Generate.toolName)] Invalid parameter \(reason)"
} }
} }
} }

View File

@ -5,8 +5,6 @@
// Created by Thibaut Schmitt on 30/08/2022. // Created by Thibaut Schmitt on 30/08/2022.
// //
import Foundation
import Foundation import Foundation
import ArgumentParser import ArgumentParser

View File

@ -30,7 +30,7 @@ struct ArchitectureGenerator {
let architectureFilePathURL = URL(fileURLWithPath: "\(filePath)/\(filename)") let architectureFilePathURL = URL(fileURLWithPath: "\(filePath)/\(filename)")
do { do {
try architectureContent.write(to: architectureFilePathURL, atomically: false, encoding: .utf8) try architectureContent.write(to: architectureFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = GenerateError.writeFile(filename, error.localizedDescription) let error = GenerateError.writeFile(filename, error.localizedDescription)
print(error.description) print(error.description)
Generate.exit(withError: error) Generate.exit(withError: error)

View File

@ -127,7 +127,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
let xcassetsPath: String let xcassetsPath: String
let extensionOutputPath: String let extensionOutputPath: String
let extensionName: String? let extensionName: String?
let extensionNameSwiftUI: String? let extensionNameUIKit: String?
let extensionSuffix: String? let extensionSuffix: String?
private let staticMembers: Bool? private let staticMembers: Bool?
@ -143,7 +143,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
xcassetsPath: String, xcassetsPath: String,
extensionOutputPath: String, extensionOutputPath: String,
extensionName: String?, extensionName: String?,
extensionNameSwiftUI: String?, extensionNameUIKit: String?,
extensionSuffix: String?, extensionSuffix: String?,
staticMembers: Bool?) { staticMembers: Bool?) {
self.inputFile = inputFile self.inputFile = inputFile
@ -151,7 +151,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
self.xcassetsPath = xcassetsPath self.xcassetsPath = xcassetsPath
self.extensionOutputPath = extensionOutputPath self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName self.extensionName = extensionName
self.extensionNameSwiftUI = extensionNameSwiftUI self.extensionNameUIKit = extensionNameUIKit
self.extensionSuffix = extensionSuffix self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers self.staticMembers = staticMembers
} }
@ -164,7 +164,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
- Xcassets path: \(xcassetsPath) - Xcassets path: \(xcassetsPath)
- Extension output path: \(extensionOutputPath) - Extension output path: \(extensionOutputPath)
- Extension name: \(extensionName ?? "-") - Extension name: \(extensionName ?? "-")
- Extension name SwiftUI: \(extensionNameSwiftUI ?? "-") - Extension name UIKit: \(extensionNameUIKit ?? "-")
- Extension suffix: \(extensionSuffix ?? "-") - Extension suffix: \(extensionSuffix ?? "-")
""" """
} }
@ -174,7 +174,7 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String let inputFile: String
let extensionOutputPath: String let extensionOutputPath: String
let extensionName: String? let extensionName: String?
let extensionNameSwiftUI: String? let extensionNameUIKit: String?
let extensionSuffix: String? let extensionSuffix: String?
let infoPlistPaths: String? let infoPlistPaths: String?
private let staticMembers: Bool? private let staticMembers: Bool?
@ -189,14 +189,14 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible {
internal init(inputFile: String, internal init(inputFile: String,
extensionOutputPath: String, extensionOutputPath: String,
extensionName: String?, extensionName: String?,
extensionNameSwiftUI: String?, extensionNameUIKit: String?,
extensionSuffix: String?, extensionSuffix: String?,
infoPlistPaths: String?, infoPlistPaths: String?,
staticMembers: Bool?) { staticMembers: Bool?) {
self.inputFile = inputFile self.inputFile = inputFile
self.extensionOutputPath = extensionOutputPath self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName self.extensionName = extensionName
self.extensionNameSwiftUI = extensionNameSwiftUI self.extensionNameUIKit = extensionNameUIKit
self.extensionSuffix = extensionSuffix self.extensionSuffix = extensionSuffix
self.infoPlistPaths = infoPlistPaths self.infoPlistPaths = infoPlistPaths
self.staticMembers = staticMembers self.staticMembers = staticMembers
@ -208,7 +208,7 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible {
- Input file: \(inputFile) - Input file: \(inputFile)
- Extension output path: \(extensionOutputPath) - Extension output path: \(extensionOutputPath)
- Extension name: \(extensionName ?? "-") - Extension name: \(extensionName ?? "-")
- Extension name SwiftUI: \(extensionNameSwiftUI ?? "-") - Extension name UIKit: \(extensionNameUIKit ?? "-")
- Extension suffix: \(extensionSuffix ?? "-") - Extension suffix: \(extensionSuffix ?? "-")
- InfoPlistPaths: \(infoPlistPaths ?? "-") - InfoPlistPaths: \(infoPlistPaths ?? "-")
""" """
@ -220,7 +220,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
let xcassetsPath: String let xcassetsPath: String
let extensionOutputPath: String let extensionOutputPath: String
let extensionName: String? let extensionName: String?
let extensionNameSwiftUI: String? let extensionNameUIKit: String?
let extensionSuffix: String? let extensionSuffix: String?
private let staticMembers: Bool? private let staticMembers: Bool?
@ -235,14 +235,14 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
xcassetsPath: String, xcassetsPath: String,
extensionOutputPath: String, extensionOutputPath: String,
extensionName: String?, extensionName: String?,
extensionNameSwiftUI: String?, extensionNameUIKit: String?,
extensionSuffix: String?, extensionSuffix: String?,
staticMembers: Bool?) { staticMembers: Bool?) {
self.inputFile = inputFile self.inputFile = inputFile
self.xcassetsPath = xcassetsPath self.xcassetsPath = xcassetsPath
self.extensionOutputPath = extensionOutputPath self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName self.extensionName = extensionName
self.extensionNameSwiftUI = extensionNameSwiftUI self.extensionNameUIKit = extensionNameUIKit
self.extensionSuffix = extensionSuffix self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers self.staticMembers = staticMembers
} }
@ -254,7 +254,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
- Xcassets path: \(xcassetsPath) - Xcassets path: \(xcassetsPath)
- Extension output path: \(extensionOutputPath) - Extension output path: \(extensionOutputPath)
- Extension name: \(extensionName ?? "-") - Extension name: \(extensionName ?? "-")
- Extension name SwiftUI: \(extensionNameSwiftUI ?? "-") - Extension name UIKit: \(extensionNameUIKit ?? "-")
- Extension suffix: \(extensionSuffix ?? "-") - Extension suffix: \(extensionSuffix ?? "-")
""" """
} }
@ -269,6 +269,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
let extensionName: String? let extensionName: String?
let extensionSuffix: String? let extensionSuffix: String?
private let staticMembers: Bool? private let staticMembers: Bool?
private let xcStrings: Bool?
var staticMembersOptions: Bool { var staticMembersOptions: Bool {
if let staticMembers = staticMembers { if let staticMembers = staticMembers {
@ -277,6 +278,13 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
return false return false
} }
var xcStringsOptions: Bool {
if let xcStrings = xcStrings {
return xcStrings
}
return false
}
internal init(inputFile: String, internal init(inputFile: String,
outputPath: String, outputPath: String,
langs: String, langs: String,
@ -284,7 +292,8 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
extensionOutputPath: String, extensionOutputPath: String,
extensionName: String?, extensionName: String?,
extensionSuffix: String?, extensionSuffix: String?,
staticMembers: Bool?) { staticMembers: Bool?,
xcStrings: Bool?) {
self.inputFile = inputFile self.inputFile = inputFile
self.outputPath = outputPath self.outputPath = outputPath
self.langs = langs self.langs = langs
@ -293,6 +302,7 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
self.extensionName = extensionName self.extensionName = extensionName
self.extensionSuffix = extensionSuffix self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers self.staticMembers = staticMembers
self.xcStrings = xcStrings
} }
var debugDescription: String { var debugDescription: String {

View File

@ -16,12 +16,15 @@ class ConfigurationFileParser {
Generate.exit(withError: error) Generate.exit(withError: error)
} }
guard let configuration = try? YAMLDecoder().decode(ConfigurationFile.self, from: data) else { do {
let error = GenerateError.invalidConfigurationFile(configurationFile) return try YAMLDecoder().decode(ConfigurationFile.self, from: data)
} catch {
let error = GenerateError.invalidConfigurationFile(
configurationFile,
error.localizedDescription.description
)
print(error.description) print(error.description)
Generate.exit(withError: error) Generate.exit(withError: error)
} }
return configuration
} }
} }

View File

@ -38,10 +38,10 @@ extension ColorsConfiguration: Runnable {
extensionName extensionName
] ]
} }
if let extensionNameSwiftUI = extensionNameSwiftUI { if let extensionNameUIKit = extensionNameUIKit {
args += [ args += [
"--extension-name-swift-ui", "--extension-name-ui-kit",
extensionNameSwiftUI extensionNameUIKit
] ]
} }
if let extensionSuffix = extensionSuffix { if let extensionSuffix = extensionSuffix {

View File

@ -34,10 +34,10 @@ extension FontsConfiguration: Runnable {
extensionName extensionName
] ]
} }
if let extensionNameSwiftUI = extensionNameSwiftUI { if let extensionNameUIKit = extensionNameUIKit {
args += [ args += [
"--extension-name-swift-ui", "--extension-name-ui-kit",
extensionNameSwiftUI extensionNameUIKit
] ]
} }

View File

@ -36,10 +36,10 @@ extension ImagesConfiguration: Runnable {
extensionName extensionName
] ]
} }
if let extensionNameSwiftUI = extensionNameSwiftUI { if let extensionNameUIKit = extensionNameUIKit {
args += [ args += [
"--extension-name-swift-ui", "--extension-name-ui-kit",
extensionNameSwiftUI extensionNameUIKit
] ]
} }
if let extensionSuffix = extensionSuffix { if let extensionSuffix = extensionSuffix {

View File

@ -10,4 +10,3 @@ import Foundation
protocol Runnable { protocol Runnable {
func run(projectDirectory: String, force: Bool) func run(projectDirectory: String, force: Bool)
} }

View File

@ -26,7 +26,9 @@ extension StringsConfiguration: Runnable {
"--extension-output-path", "--extension-output-path",
extensionOutputPath.prependIfRelativePath(projectDirectory), extensionOutputPath.prependIfRelativePath(projectDirectory),
"--static-members", "--static-members",
"\(staticMembersOptions)" "\(staticMembersOptions)",
"--xc-strings",
"\(xcStringsOptions)"
] ]
if let extensionName = extensionName { if let extensionName = extensionName {

View File

@ -18,7 +18,7 @@ extension FileManager {
for case let fileURL as URL in enumerator { for case let fileURL as URL in enumerator {
do { do {
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey]) let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
if fileAttributes.isRegularFile! { if fileAttributes.isRegularFile! {
files.append(fileURL.relativePath) files.append(fileURL.relativePath)
} }
@ -41,7 +41,7 @@ extension FileManager {
for case let fileURL as URL in enumerator { for case let fileURL as URL in enumerator {
do { do {
let fileAttributes = try fileURL.resourceValues(forKeys:[.isDirectoryKey]) let fileAttributes = try fileURL.resourceValues(forKeys: [.isDirectoryKey])
if fileAttributes.isDirectory! && fileURL.lastPathComponent.hasSuffix(".imageset") { if fileAttributes.isDirectory! && fileURL.lastPathComponent.hasSuffix(".imageset") {
files.append(fileURL.lastPathComponent) files.append(fileURL.lastPathComponent)
} }

View File

@ -29,7 +29,7 @@ class ImageExtensionGenerator {
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do { do {
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription) let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription)
print(error.description) print(error.description)
Images.exit(withError: error) Images.exit(withError: error)

View File

@ -8,9 +8,12 @@
import Foundation import Foundation
import ToolCore import ToolCore
class XcassetsGenerator { enum OutputImageExtension: String {
case png
case svg
}
static let outputImageExtension = "png" class XcassetsGenerator {
let forceGeneration: Bool let forceGeneration: Bool
@ -60,13 +63,20 @@ class XcassetsGenerator {
generatedAssetsPaths.append(imagesetName) generatedAssetsPaths.append(imagesetName)
// Generate output images path // Generate output images path
let output1x = "\(imagesetPath)/\(parsedImage.name).\(XcassetsGenerator.outputImageExtension)" let output1x = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.png.rawValue)"
let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(XcassetsGenerator.outputImageExtension)" let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(OutputImageExtension.png.rawValue)"
let output3x = "\(imagesetPath)/\(parsedImage.name)@3x.\(XcassetsGenerator.outputImageExtension)" let output3x = "\(imagesetPath)/\(parsedImage.name)@3x.\(OutputImageExtension.png.rawValue)"
// Check if we need to convert image // Check if we need to convert image
guard self.shouldGenerate(inputImagePath: imageData.path, xcassetImagePath: output1x) else {
//print("\(parsedImage.name) -> Not regenerating") var needToGenerateForSvg = false
if imageData.ext == "svg" && !parsedImage.imageExtensions.contains(.png) {
needToGenerateForSvg = true
}
guard self.shouldGenerate(inputImagePath: imageData.path, xcassetImagePath: output1x, needToGenerateForSvg: needToGenerateForSvg) else {
print("\(parsedImage.name) -> Not regenerating")
return return
} }
@ -80,11 +90,22 @@ class XcassetsGenerator {
print(error.description) print(error.description)
Images.exit(withError: error) Images.exit(withError: error)
} }
} else {
do {
let documentsDirectory = try fileManager.contentsOfDirectory(atPath: imagesetPath)
for filePath in documentsDirectory {
try fileManager.removeItem(atPath: "\(imagesetPath)/\(filePath)")
}
} catch {
print("Error deleting previous assets")
}
} }
// Convert image
let convertArguments = parsedImage.convertArguments let convertArguments = parsedImage.convertArguments
if imageData.ext == "svg" { if imageData.ext == "svg" {
if parsedImage.imageExtensions.contains(.png) {
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -h 300 -o path/to/output.png // /usr/local/bin/rsvg-convert path/to/image.png -w 200 -h 300 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -o path/to/output.png // /usr/local/bin/rsvg-convert path/to/image.png -w 200 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -h 300 -o path/to/output.png // /usr/local/bin/rsvg-convert path/to/image.png -h 300 -o path/to/output.png
@ -103,6 +124,21 @@ class XcassetsGenerator {
Shell.shell(command1x) Shell.shell(command1x)
Shell.shell(command2x) Shell.shell(command2x)
Shell.shell(command3x) Shell.shell(command3x)
} else {
let output = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.svg.rawValue)"
let tempURL = URL(fileURLWithPath: output)
do {
if FileManager.default.fileExists(atPath: tempURL.path) {
try FileManager.default.removeItem(atPath: tempURL.path)
}
try FileManager.default.copyItem(atPath: imageData.path, toPath: tempURL.path)
} catch {
print(error.localizedDescription)
}
}
} else { } else {
// convert path/to/image.png -resize 200x300 path/to/output.png // convert path/to/image.png -resize 200x300 path/to/output.png
// convert path/to/image.png -resize 200x path/to/output.png // convert path/to/image.png -resize 200x path/to/output.png
@ -119,7 +155,7 @@ class XcassetsGenerator {
} }
// Write Content.json // Write Content.json
let imagesetContentJson = parsedImage.contentJson guard let imagesetContentJson = parsedImage.generateContentJson(isVector: imageData.ext == "svg") else { return }
let contentJsonFilePath = "\(imagesetPath)/Contents.json" let contentJsonFilePath = "\(imagesetPath)/Contents.json"
let contentJsonFilePathURL = URL(fileURLWithPath: contentJsonFilePath) let contentJsonFilePathURL = URL(fileURLWithPath: contentJsonFilePath)
@ -161,8 +197,8 @@ class XcassetsGenerator {
// MARK: - Helpers: bypass generation // MARK: - Helpers: bypass generation
private func shouldGenerate(inputImagePath: String, xcassetImagePath: String) -> Bool { private func shouldGenerate(inputImagePath: String, xcassetImagePath: String, needToGenerateForSvg: Bool) -> Bool {
if forceGeneration { if forceGeneration || needToGenerateForSvg {
return true return true
} }

View File

@ -21,8 +21,8 @@ struct Images: ParsableCommand {
// MARK: - Static // MARK: - Static
static let toolName = "Images" static let toolName = "Images"
static let defaultExtensionName = "UIImage" static let defaultExtensionName = "Image"
static let defaultExtensionNameSUI = "Image" static let defaultExtensionNameUIKit = "UIImage"
// MARK: - Command Options // MARK: - Command Options
@ -58,14 +58,14 @@ struct Images: ParsableCommand {
inputFilename: options.inputFilenameWithoutExt, inputFilename: options.inputFilenameWithoutExt,
extensionName: options.extensionName, extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath, extensionFilePath: options.extensionFilePath,
isSwiftUI: false) isSwiftUI: true)
ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate, ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate,
staticVar: options.staticMembers, staticVar: options.staticMembers,
inputFilename: options.inputFilenameWithoutExt, inputFilename: options.inputFilenameWithoutExt,
extensionName: options.extensionNameSwiftUI, extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathSwiftUI, extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: true) isSwiftUI: false)
print("[\(Self.toolName)] Images generated") print("[\(Self.toolName)] Images generated")
} }
@ -90,7 +90,7 @@ struct Images: ParsableCommand {
_ = Images.getSvgConverterPath() _ = Images.getSvgConverterPath()
// Extension for UIKit and SwiftUI should have different name // Extension for UIKit and SwiftUI should have different name
guard options.extensionName != options.extensionNameSwiftUI else { guard options.extensionName != options.extensionNameUIKit else {
let error = ImagesError.extensionNamesCollision(options.extensionName) let error = ImagesError.extensionNamesCollision(options.extensionName)
print(error.description) print(error.description)
Images.exit(withError: error) Images.exit(withError: error)

View File

@ -27,11 +27,11 @@ struct ImagesOptions: ParsableArguments {
@Option(help: "Tell if it will generate static properties or not") @Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate an UIImage extension.") @Option(help: "Extension name. If not specified, it will generate an Image extension.")
var extensionName: String = Images.defaultExtensionName var extensionName: String = Images.defaultExtensionName
@Option(help: "Extension name. If not specified, it will generate an Image extension.") @Option(help: "Extension name. If not specified, it will generate an UIImage extension.")
var extensionNameSwiftUI: String = Images.defaultExtensionNameSUI var extensionNameUIKit: String = Images.defaultExtensionNameUIKit
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Image{extensionSuffix}.swift") @Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Image{extensionSuffix}.swift")
var extensionSuffix: String? var extensionSuffix: String?
@ -41,7 +41,7 @@ struct ImagesOptions: ParsableArguments {
extension ImagesOptions { extension ImagesOptions {
// MARK: - UIKit // MARK: - SwiftUI
var extensionFileName: String { var extensionFileName: String {
if let extensionSuffix = extensionSuffix { if let extensionSuffix = extensionSuffix {
@ -54,17 +54,17 @@ extension ImagesOptions {
"\(extensionOutputPath)/\(extensionFileName)" "\(extensionOutputPath)/\(extensionFileName)"
} }
// MARK: - SwiftUI // MARK: - UIKit
var extensionFileNameSwiftUI: String { var extensionFileNameUIKit: String {
if let extensionSuffix = extensionSuffix { if let extensionSuffix = extensionSuffix {
return "\(extensionNameSwiftUI)+\(extensionSuffix).swift" return "\(extensionNameUIKit)+\(extensionSuffix).swift"
} }
return "\(extensionNameSwiftUI).swift" return "\(extensionNameUIKit).swift"
} }
var extensionFilePathSwiftUI: String { var extensionFilePathUIKit: String {
"\(extensionOutputPath)/\(extensionFileNameSwiftUI)" "\(extensionOutputPath)/\(extensionFileNameUIKit)"
} }
// MARK: - // MARK: -

View File

@ -0,0 +1,76 @@
//
// ImageContent.swift
//
//
// Created by Quentin Bandera on 19/04/2024.
//
import Foundation
enum TemplateRenderingIntent: String, Codable {
case template
case original
}
struct AssetContent: Codable, Equatable {
let images: [AssetImageDescription]
let info: AssetInfo
let properties: AssetProperties?
init(
images: [AssetImageDescription],
info: AssetInfo,
properties: AssetProperties? = nil
) {
self.images = images
self.info = info
self.properties = properties
}
static func == (lhs: AssetContent, rhs: AssetContent) -> Bool {
guard lhs.images.count == rhs.images.count else { return false }
let lhsImages = lhs.images.sorted(by: { $0.filename < $1.filename })
let rhsImages = rhs.images.sorted(by: { $0.filename < $1.filename })
return lhsImages == rhsImages
}
}
struct AssetImageDescription: Codable, Equatable {
let idiom: String
let scale: String?
let filename: String
init(
idiom: String,
scale: String? = nil,
filename: String
) {
self.idiom = idiom
self.scale = scale
self.filename = filename
}
}
struct AssetInfo: Codable, Equatable {
let version: Int
let author: String
}
struct AssetProperties: Codable, Equatable {
let preservesVectorRepresentation: Bool
let templateRenderingIntent: TemplateRenderingIntent?
init(
preservesVectorRepresentation: Bool,
templateRenderingIntent: TemplateRenderingIntent? = nil
) {
self.preservesVectorRepresentation = preservesVectorRepresentation
self.templateRenderingIntent = templateRenderingIntent
}
enum CodingKeys: String, CodingKey {
case preservesVectorRepresentation = "preserves-vector-representation"
case templateRenderingIntent = "template-rendering-intent"
}
}

View File

@ -7,11 +7,30 @@
import Foundation import Foundation
enum ImageExtension: String {
case png
}
struct ParsedImage { struct ParsedImage {
let name: String let name: String
let tags: String let tags: String
let width: Int let width: Int
let height: Int let height: Int
let imageExtensions: [ImageExtension]
init(
name: String,
tags: String,
width: Int,
height: Int,
imageExtensions: [ImageExtension] = []
) {
self.name = name
self.tags = tags
self.width = width
self.height = height
self.imageExtensions = imageExtensions
}
// MARK: - Convert // MARK: - Convert
@ -42,33 +61,68 @@ struct ParsedImage {
// MARK: - Assets // MARK: - Assets
var contentJson: String { func generateContentJson(isVector: Bool) -> String? {
""" let encoder = JSONEncoder()
{ encoder.outputFormatting = .prettyPrinted
"images" : [
{ let imageContent = generateImageContent(isVector: isVector)
"idiom" : "universal",
"scale" : "1x", guard let data = try? encoder.encode(imageContent) else {
"filename" : "\(name).\(XcassetsGenerator.outputImageExtension)" let error = ImagesError.writeFile("Contents.json", "Error encoding json file")
}, print(error.description)
{ Images.exit(withError: error)
"idiom" : "universal",
"scale" : "2x",
"filename" : "\(name)@2x.\(XcassetsGenerator.outputImageExtension)"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "\(name)@3x.\(XcassetsGenerator.outputImageExtension)"
} }
return String(data: data, encoding: .utf8)
}
func generateImageContent(isVector: Bool) -> AssetContent {
if !imageExtensions.contains(.png) && isVector {
//Generate svg
return AssetContent(
images: [
AssetImageDescription(
idiom: "universal",
filename: "\(name).\(OutputImageExtension.svg.rawValue)"
)
], ],
"info" : { info: AssetInfo(
"version" : 1, version: 1,
"author" : "ResgenSwift-Imagium" author: "ResgenSwift-Imagium"
),
properties: AssetProperties(
preservesVectorRepresentation: true,
templateRenderingIntent: .original
)
)
} else {
//Generate png
return AssetContent(
images: [
AssetImageDescription(
idiom: "universal",
scale: "1x",
filename: "\(name).\(OutputImageExtension.png.rawValue)"
),
AssetImageDescription(
idiom: "universal",
scale: "2x",
filename: "\(name)@2x.\(OutputImageExtension.png.rawValue)"
),
AssetImageDescription(
idiom: "universal",
scale: "3x",
filename: "\(name)@3x.\(OutputImageExtension.png.rawValue)"
)
],
info: AssetInfo(
version: 1,
author: "ResgenSwift-Imagium"
)
)
} }
} }
"""
}
// MARK: - Extension property // MARK: - Extension property

View File

@ -39,10 +39,20 @@ class ImageFileParser {
return Int(splittedLine[3])! return Int(splittedLine[3])!
}() }()
let image = ParsedImage(name: String(splittedLine[1]), tags: String(splittedLine[0]), width: width, height: height) var imageExtensions: [ImageExtension] = []
splittedLine.forEach { stringExtension in
if let imageExtension = ImageExtension(rawValue: String(stringExtension)) {
imageExtensions.append(imageExtension)
}
}
let image = ParsedImage(name: String(splittedLine[1]), tags: String(splittedLine[0]), width: width, height: height, imageExtensions: imageExtensions)
imagesToGenerate.append(image) imagesToGenerate.append(image)
} }
print(imagesToGenerate)
return imagesToGenerate.filter { return imagesToGenerate.filter {
$0.tags.contains(platform.rawValue) $0.tags.contains(platform.rawValue)
} }

View File

@ -18,6 +18,7 @@ class StringsFileGenerator {
tags: [String], tags: [String],
outputPath: String, outputPath: String,
inputFilenameWithoutExt: String) { inputFilenameWithoutExt: String) {
var stringsFilesContent = [String: String]() var stringsFilesContent = [String: String]()
for lang in langs { for lang in langs {
stringsFilesContent[lang] = Self.generateStringsFileContent(lang: lang, stringsFilesContent[lang] = Self.generateStringsFileContent(lang: lang,
@ -34,7 +35,7 @@ class StringsFileGenerator {
let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath) let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
do { do {
try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8) try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath) let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
print(error.description) print(error.description)
Stringium.exit(withError: error) Stringium.exit(withError: error)
@ -42,6 +43,31 @@ class StringsFileGenerator {
} }
} }
static func writeXcStringsFiles(sections: [Section],
langs: [String],
defaultLang: String,
tags: [String],
outputPath: String,
inputFilenameWithoutExt: String) {
let fileContent: String = Self.generateXcStringsFileContent(
langs: langs,
defaultLang: defaultLang,
tags: tags,
sections: sections
)
let stringsFilePath = "\(outputPath)/\(inputFilenameWithoutExt).xcstrings"
let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
do {
try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
} catch let error {
let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
print(error.description)
Stringium.exit(withError: error)
}
}
static func generateStringsFileContent(lang: String, static func generateStringsFileContent(lang: String,
defaultLang: String, defaultLang: String,
tags inputTags: [String], tags inputTags: [String],
@ -92,6 +118,117 @@ class StringsFileGenerator {
return stringsFileContent return stringsFileContent
} }
// MARK: - XcStrings Generation
static func generateXcStringsFileContent(langs: [String],
defaultLang: String,
tags inputTags: [String],
sections: [Section]) -> String {
let rootObject = generateRootObject(langs: langs, defaultLang: defaultLang, tags: inputTags, sections: sections)
let file = generateXcStringsFileContentFromRootObject(rootObject: rootObject)
return file
}
static func generateXcStringsFileContentFromRootObject(rootObject: Root) -> String {
do {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
let json = try encoder.encode(rootObject)
if let jsonString = String(data: json, encoding: .utf8) {
return jsonString
}
} catch {
debugPrint("Failed to encode: \(error)")
}
return ""
}
static func generateRootObject(langs: [String],
defaultLang: String,
tags inputTags: [String],
sections: [Section]) -> Root {
var xcStringDefinitionTab: [XCStringDefinition] = []
sections.forEach { section in
// Check that at least one string will be generated
guard section.hasOneOrMoreMatchingTags(tags: inputTags) else {
return // Go to next section
}
section.definitions.forEach { definition in
var skipDefinition = false
var isNoTranslation = false
var localizationTab: [XCStringLocalization] = []
if definition.hasOneOrMoreMatchingTags(inputTags: inputTags) == false {
skipDefinition = true
}
if definition.tags.contains(Stringium.noTranslationTag) {
isNoTranslation = true
}
if !skipDefinition {
if isNoTranslation {
// Search for langs in yaml
for lang in langs {
if let value = definition.translations[defaultLang], !value.isEmpty {
let localization = XCStringLocalization(
lang: lang,
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(state: "translated", value: value)
)
)
localizationTab.append(localization)
}
}
} else {
// Search for langs in twine
for (lang, value) in definition.translations where !value.isEmpty {
let localization = XCStringLocalization(
lang: lang,
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(state: "translated", value: value)
)
)
localizationTab.append(localization)
}
}
let xcStringDefinition = XCStringDefinition(
title: definition.name,
content: XCStringDefinitionContent(
comment: definition.comment,
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: localizationTab
)
)
)
xcStringDefinitionTab.append(xcStringDefinition)
}
}
}
let xcStringContainer = XCStringDefinitionContainer(strings: xcStringDefinitionTab)
return Root(
sourceLanguage: defaultLang,
strings: xcStringContainer,
version: "1.0"
)
}
// MARK: - Extension file // MARK: - Extension file
static func writeExtensionFiles(sections: [Section], static func writeExtensionFiles(sections: [Section],
@ -115,7 +252,7 @@ class StringsFileGenerator {
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do { do {
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
print(error.description) print(error.description)
Stringium.exit(withError: error) Stringium.exit(withError: error)

View File

@ -22,7 +22,7 @@ class TagsGenerator {
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath) let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do { do {
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8) try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) { } catch let error {
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription) let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
print(error.description) print(error.description)
Stringium.exit(withError: error) Stringium.exit(withError: error)

View File

@ -37,7 +37,7 @@ class Definition {
} }
func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool { func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool {
if Set(inputTags).intersection(Set(self.tags)).isEmpty { if Set(inputTags).isDisjoint(with: tags) {
return false return false
} }
return true return true
@ -84,22 +84,29 @@ class Definition {
return (inputParameters: inputParameters, translationArguments: translationArguments) return (inputParameters: inputParameters, translationArguments: translationArguments)
} }
private func getBaseProperty(lang: String, translation: String, isStatic: Bool) -> String { private func getBaseProperty(lang: String, translation: String, isStatic: Bool, comment: String?) -> String {
""" """
/// Translation in \(lang) : /// Translation in \(lang) :
/// \(translation) /// \(translation)
///
/// Comment :
/// \(comment?.isEmpty == false ? comment! : "No comment")
\(isStatic ? "static ": "")var \(name): String { \(isStatic ? "static ": "")var \(name): String {
NSLocalizedString("\(name)", tableName: kStringsFileName, bundle: Bundle.main, value: "\(translation)", comment: "") NSLocalizedString("\(name)", tableName: kStringsFileName, bundle: Bundle.main, value: "\(translation)", comment: "\(comment ?? "")")
} }
""" """
} }
private func getBaseMethod(lang: String, translation: String, isStatic: Bool, inputParameters: [String], translationArguments: [String]) -> String { private func getBaseMethod(lang: String, translation: String, isStatic: Bool, inputParameters: [String], translationArguments: [String], comment: String?) -> String {
"""
"""
/// Translation in \(lang) : /// Translation in \(lang) :
/// \(translation) /// \(translation)
///
/// Comment :
/// \(comment?.isEmpty == false ? comment! : "No comment")
\(isStatic ? "static ": "")func \(name)(\(inputParameters.joined(separator: ", "))) -> String { \(isStatic ? "static ": "")func \(name)(\(inputParameters.joined(separator: ", "))) -> String {
String(format: \(isStatic ? "Self" : "self").\(name), \(translationArguments.joined(separator: ", "))) String(format: \(isStatic ? "Self" : "self").\(name), \(translationArguments.joined(separator: ", ")))
} }
@ -114,7 +121,12 @@ class Definition {
} }
// Generate property // Generate property
let property = getBaseProperty(lang: lang, translation: translation, isStatic: false) let property = getBaseProperty(
lang: lang,
translation: translation,
isStatic: false,
comment: self.comment
)
// Generate method // Generate method
var method = "" var method = ""
@ -123,7 +135,8 @@ class Definition {
translation: translation, translation: translation,
isStatic: false, isStatic: false,
inputParameters: parameters.inputParameters, inputParameters: parameters.inputParameters,
translationArguments: parameters.translationArguments) translationArguments: parameters.translationArguments,
comment: self.comment)
} }
return property + method return property + method
@ -137,7 +150,12 @@ class Definition {
} }
// Generate property // Generate property
let property = getBaseProperty(lang: lang, translation: translation, isStatic: true) let property = getBaseProperty(
lang: lang,
translation: translation,
isStatic: true,
comment: self.comment
)
// Generate method // Generate method
var method = "" var method = ""
@ -146,7 +164,8 @@ class Definition {
translation: translation, translation: translation,
isStatic: true, isStatic: true,
inputParameters: parameters.inputParameters, inputParameters: parameters.inputParameters,
translationArguments: parameters.translationArguments) translationArguments: parameters.translationArguments,
comment: self.comment)
} }
return property + method return property + method
@ -164,6 +183,10 @@ class Definition {
return """ return """
/// Translation in \(lang) : /// Translation in \(lang) :
/// \(translation) /// \(translation)
///
/// Comment :
/// \(comment?.isEmpty == false ? comment! : "No comment")
var \(name): String { var \(name): String {
"\(translation)" "\(translation)"
} }
@ -180,6 +203,9 @@ class Definition {
return """ return """
/// Translation in \(lang) : /// Translation in \(lang) :
/// \(translation) /// \(translation)
///
/// Comment :
/// \(comment?.isEmpty == false ? comment! : "No comment")
static var \(name): String { static var \(name): String {
"\(translation)" "\(translation)"
} }

View File

@ -0,0 +1,109 @@
//
// XcString.swift
//
//
// Created by Quentin Bandera on 12/04/2024.
//
import SwiftUI
struct DynamicKey: CodingKey {
var intValue: Int?
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
}
struct Root: Codable, Equatable {
let sourceLanguage: String
let strings: XCStringDefinitionContainer
let version: String
}
struct XCStringDefinitionContainer: Codable, Equatable {
let strings: [XCStringDefinition]
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DynamicKey.self)
for str in strings {
if let codingKey = DynamicKey(stringValue: str.title) {
try container.encode(str.content, forKey: codingKey)
}
}
}
static func == (lhs: XCStringDefinitionContainer, rhs: XCStringDefinitionContainer) -> Bool {
return lhs.strings.sorted(by: {
$0.title < $1.title
}) == rhs.strings.sorted(by: { $0.title < $1.title })
}
}
struct XCStringDefinition: Codable, Equatable {
let title: String // json key -> custom encoding methods
let content: XCStringDefinitionContent
}
struct XCStringDefinitionContent: Codable, Equatable {
let comment: String?
let extractionState: String
var localizations: XCStringLocalizationContainer
init(comment: String? = nil, extractionState: String, localizations: XCStringLocalizationContainer) {
self.comment = comment
self.extractionState = extractionState
self.localizations = localizations
}
}
struct XCStringLocalizationContainer: Codable, Equatable {
let localizations: [XCStringLocalization]
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DynamicKey.self)
for loca in localizations {
if let codingKey = DynamicKey(stringValue: loca.lang) {
try container.encode(loca.content, forKey: codingKey)
}
}
}
static func == (lhs: XCStringLocalizationContainer, rhs: XCStringLocalizationContainer) -> Bool {
return lhs.localizations.count == rhs.localizations.count && lhs.localizations.sorted(by: { $0.lang < $1.lang }) == rhs.localizations.sorted(by: { $0.lang < $1.lang })
}
}
struct XCStringLocalization: Codable, Equatable {
let lang: String // json key -> custom encoding method
let content: XCStringLocalizationLangContent
}
struct XCStringLocalizationLangContent: Codable, Equatable {
let stringUnit: DefaultStringUnit
}
//enum VarationOrStringUnit: Encodable {
// case variations([Varation])
// case stringUnit: (DefaultStringUnit)
//
// func encode(to encoder: any Encoder) throws {
// if let varations {
//
// } else if let {
//
// }
// }
//}
struct DefaultStringUnit: Codable, Equatable {
let state: String
let value: String
}

View File

@ -43,12 +43,25 @@ struct Stringium: ParsableCommand {
let sections = TwineFileParser.parse(options.inputFile) let sections = TwineFileParser.parse(options.inputFile)
// Generate strings files // Generate strings files
print(options.xcStrings)
if !options.xcStrings {
print("[\(Self.toolName)] Will generate strings")
StringsFileGenerator.writeStringsFiles(sections: sections, StringsFileGenerator.writeStringsFiles(sections: sections,
langs: options.langs, langs: options.langs,
defaultLang: options.defaultLang, defaultLang: options.defaultLang,
tags: options.tags, tags: options.tags,
outputPath: options.stringsFileOutputPath, outputPath: options.stringsFileOutputPath,
inputFilenameWithoutExt: options.inputFilenameWithoutExt) inputFilenameWithoutExt: options.inputFilenameWithoutExt)
} else {
print("[\(Self.toolName)] Will generate xcStrings")
StringsFileGenerator.writeXcStringsFiles(sections: sections,
langs: options.langs,
defaultLang: options.defaultLang,
tags: options.tags,
outputPath: options.stringsFileOutputPath,
inputFilenameWithoutExt: options.inputFilenameWithoutExt)
}
// Generate extension // Generate extension
StringsFileGenerator.writeExtensionFiles(sections: sections, StringsFileGenerator.writeExtensionFiles(sections: sections,

View File

@ -12,7 +12,7 @@ struct StringiumOptions: ParsableArguments {
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation") @Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
var forceGeneration = false var forceGeneration = false
@Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) @Argument(help: "Input files where strings are defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var inputFile: String var inputFile: String
@Option(name: .customLong("output-path"), help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() }) @Option(name: .customLong("output-path"), help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
@ -33,6 +33,9 @@ struct StringiumOptions: ParsableArguments {
@Option(help: "Tell if it will generate static properties or not") @Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false var staticMembers: Bool = false
@Option(help: "Tell if it will generate xcStrings file or not")
var xcStrings: Bool = false
@Option(help: "Extension name. If not specified, it will generate an String extension.") @Option(help: "Extension name. If not specified, it will generate an String extension.")
var extensionName: String = Stringium.defaultExtensionName var extensionName: String = Stringium.defaultExtensionName

View File

@ -27,4 +27,3 @@ struct Strings: ParsableCommand {
} }
//Strings.main() //Strings.main()

View File

@ -18,7 +18,6 @@ struct Tags: ParsableCommand {
version: ResgenSwiftVersion version: ResgenSwiftVersion
) )
// MARK: - Static // MARK: - Static
static let toolName = "Tags" static let toolName = "Tags"

View File

@ -89,4 +89,19 @@ public extension String {
func uppercasedFirst() -> String { func uppercasedFirst() -> String {
prefix(1).uppercased() + dropFirst() prefix(1).uppercased() + dropFirst()
} }
func replacingFirstOccurrence(of: String, with: String) -> Self {
if let range = self.range(of: of) {
let tmp = self.replacingOccurrences(
of: of,
with: with,
options: .literal,
range: range
)
return tmp
}
return self
}
} }

View File

@ -51,7 +51,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
] ]
// When // When
AnalyticsGenerator.targets = [Analytics.TargetType.firebase] AnalyticsGenerator.targets = [TrackerType.firebase]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"], tags: ["ios", "iosonly"],
staticVar: false, staticVar: false,
@ -216,7 +216,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
] ]
// When // When
AnalyticsGenerator.targets = [Analytics.TargetType.matomo] AnalyticsGenerator.targets = [TrackerType.matomo]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"], tags: ["ios", "iosonly"],
staticVar: false, staticVar: false,
@ -409,7 +409,7 @@ final class AnalyticsGeneratorTests: XCTestCase {
] ]
// When // When
AnalyticsGenerator.targets = [Analytics.TargetType.matomo, Analytics.TargetType.firebase] AnalyticsGenerator.targets = [TrackerType.matomo, TrackerType.firebase]
let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree], let extensionContent = AnalyticsGenerator.getExtensionContent(sections: [sectionOne, sectionTwo, sectionThree],
tags: ["ios", "iosonly"], tags: ["ios", "iosonly"],
staticVar: false, staticVar: false,

View File

@ -1,5 +1,5 @@
// //
// File.swift // DiffString.swift
// //
// //
// Created by Loris Perret on 06/12/2023. // Created by Loris Perret on 06/12/2023.
@ -7,7 +7,6 @@
import Foundation import Foundation
/// Find first differing character between two strings /// Find first differing character between two strings
/// ///
/// :param: s1 First String /// :param: s1 First String

View File

@ -21,7 +21,7 @@ final class ColorsConfigurationTests: XCTestCase {
xcassetsPath: "path/to/assets.xcassets", xcassetsPath: "path/to/assets.xcassets",
extensionOutputPath: "Colors/Generated", extensionOutputPath: "Colors/Generated",
extensionName: nil, extensionName: nil,
extensionNameSwiftUI: nil, extensionNameUIKit: nil,
extensionSuffix: nil, extensionSuffix: nil,
staticMembers: false) staticMembers: false)
// When // When
@ -50,7 +50,7 @@ final class ColorsConfigurationTests: XCTestCase {
xcassetsPath: "path/to/assets.xcassets", xcassetsPath: "path/to/assets.xcassets",
extensionOutputPath: "Colors/Generated", extensionOutputPath: "Colors/Generated",
extensionName: "AppUIColor", extensionName: "AppUIColor",
extensionNameSwiftUI: "AppColor", extensionNameUIKit: "AppColor",
extensionSuffix: "Testing", extensionSuffix: "Testing",
staticMembers: false) staticMembers: false)
// When // When
@ -70,7 +70,7 @@ final class ColorsConfigurationTests: XCTestCase {
"false", "false",
"--extension-name", "--extension-name",
"AppUIColor", "AppUIColor",
"--extension-name-swift-ui", "--extension-name-ui-kit",
"AppColor", "AppColor",
"--extension-suffix", "--extension-suffix",
"Testing", "Testing",

View File

@ -19,7 +19,7 @@ final class FontsConfigurationTests: XCTestCase {
let testingConfiguration = FontsConfiguration(inputFile: "path/to/fonts.txt", let testingConfiguration = FontsConfiguration(inputFile: "path/to/fonts.txt",
extensionOutputPath: "Fonts/Generated", extensionOutputPath: "Fonts/Generated",
extensionName: nil, extensionName: nil,
extensionNameSwiftUI: nil, extensionNameUIKit: nil,
extensionSuffix: nil, extensionSuffix: nil,
infoPlistPaths: nil, infoPlistPaths: nil,
staticMembers: nil) staticMembers: nil)
@ -43,7 +43,7 @@ final class FontsConfigurationTests: XCTestCase {
let testingConfiguration = FontsConfiguration(inputFile: "path/to/fonts.txt", let testingConfiguration = FontsConfiguration(inputFile: "path/to/fonts.txt",
extensionOutputPath: "Fonts/Generated", extensionOutputPath: "Fonts/Generated",
extensionName: "AppUIFont", extensionName: "AppUIFont",
extensionNameSwiftUI: "AppFont", extensionNameUIKit: "AppFont",
extensionSuffix: "Testing", extensionSuffix: "Testing",
infoPlistPaths: "path/to/plist1.plist path/to/plist2.plist", infoPlistPaths: "path/to/plist1.plist path/to/plist2.plist",
staticMembers: true) staticMembers: true)
@ -60,7 +60,7 @@ final class FontsConfigurationTests: XCTestCase {
"true", "true",
"--extension-name", "--extension-name",
"AppUIFont", "AppUIFont",
"--extension-name-swift-ui", "--extension-name-ui-kit",
"AppFont", "AppFont",
"--extension-suffix", "--extension-suffix",
"Testing", "Testing",

View File

@ -20,7 +20,7 @@ final class ImagesConfigurationTests: XCTestCase {
xcassetsPath: "path/to/assets.xcassets", xcassetsPath: "path/to/assets.xcassets",
extensionOutputPath: "Images/Generated", extensionOutputPath: "Images/Generated",
extensionName: nil, extensionName: nil,
extensionNameSwiftUI: nil, extensionNameUIKit: nil,
extensionSuffix: nil, extensionSuffix: nil,
staticMembers: nil) staticMembers: nil)
@ -47,7 +47,7 @@ final class ImagesConfigurationTests: XCTestCase {
xcassetsPath: "path/to/assets.xcassets", xcassetsPath: "path/to/assets.xcassets",
extensionOutputPath: "Images/Generated", extensionOutputPath: "Images/Generated",
extensionName: "AppUIImage", extensionName: "AppUIImage",
extensionNameSwiftUI: "AppImage", extensionNameUIKit: "AppImage",
extensionSuffix: "Testing", extensionSuffix: "Testing",
staticMembers: true) staticMembers: true)
@ -66,7 +66,7 @@ final class ImagesConfigurationTests: XCTestCase {
"true", "true",
"--extension-name", "--extension-name",
"AppUIImage", "AppUIImage",
"--extension-name-swift-ui", "--extension-name-ui-kit",
"AppImage", "AppImage",
"--extension-suffix", "--extension-suffix",
"Testing", "Testing",

View File

@ -17,8 +17,8 @@ class ImageFileParserTests: XCTestCase {
# #
# SMAAS Support # SMAAS Support
# #
id image_one 25 ? id image_one 25 ? png
di image_two ? 50 di image_two ? 50 webp png
d image_three 25 ? d image_three 25 ?
d image_four 75 ? d image_four 75 ?
""" """
@ -38,6 +38,7 @@ class ImageFileParserTests: XCTestCase {
XCTAssertEqual(firstImage!.tags, "id") XCTAssertEqual(firstImage!.tags, "id")
XCTAssertEqual(firstImage!.width, 25) XCTAssertEqual(firstImage!.width, 25)
XCTAssertEqual(firstImage!.height, -1) XCTAssertEqual(firstImage!.height, -1)
XCTAssertEqual(firstImage!.imageExtensions, [.png])
let secondImage = parsedImages.first { let secondImage = parsedImages.first {
$0.name == "image_two" $0.name == "image_two"
@ -46,5 +47,6 @@ class ImageFileParserTests: XCTestCase {
XCTAssertEqual(secondImage!.tags, "di") XCTAssertEqual(secondImage!.tags, "di")
XCTAssertEqual(secondImage!.width, -1) XCTAssertEqual(secondImage!.width, -1)
XCTAssertEqual(secondImage!.height, 50) XCTAssertEqual(secondImage!.height, 50)
XCTAssertEqual(firstImage!.imageExtensions, [.png])
} }
} }

View File

@ -127,35 +127,77 @@ final class ParsedImageTests: XCTestCase {
height: 10) height: 10)
// When // When
let property = parsedImage.contentJson let property = parsedImage.generateImageContent(isVector: false)
// Expect // Expect
let expect = """ let expect = AssetContent(
{ images: [
"images" : [ AssetImageDescription(
{ idiom: "universal",
"idiom" : "universal", scale: "1x",
"scale" : "1x", filename: "\(parsedImage.name).\(OutputImageExtension.png.rawValue)"
"filename" : "\(imageName).\(XcassetsGenerator.outputImageExtension)" ),
}, AssetImageDescription(
{ idiom: "universal",
"idiom" : "universal", scale: "2x",
"scale" : "2x", filename: "\(parsedImage.name)@2x.\(OutputImageExtension.png.rawValue)"
"filename" : "\(imageName)@2x.\(XcassetsGenerator.outputImageExtension)" ),
}, AssetImageDescription(
{ idiom: "universal",
"idiom" : "universal", scale: "3x",
"scale" : "3x", filename: "\(parsedImage.name)@3x.\(OutputImageExtension.png.rawValue)"
"filename" : "\(imageName)@3x.\(XcassetsGenerator.outputImageExtension)" )
}
], ],
"info" : { info: AssetInfo(
"version" : 1, version: 1,
"author" : "ResgenSwift-Imagium" author: "ResgenSwift-Imagium"
} )
} )
"""
XCTAssertEqual(property.adaptForXCTest(), expect.adaptForXCTest()) XCTAssertEqual(property, expect)
}
func testAssetPng() {
// Given
let imageName = "the_name"
let parsedImage = ParsedImage(name: imageName,
tags: "id",
width: 10,
height: 10,
imageExtensions: [.png])
// When
let property = parsedImage.generateImageContent(isVector: false)
// Expect
let expect = AssetContent(
images: [
AssetImageDescription(
idiom: "universal",
scale: "1x",
filename: "\(parsedImage.name).\(OutputImageExtension.png.rawValue)"
),
AssetImageDescription(
idiom: "universal",
scale: "2x",
filename: "\(parsedImage.name)@2x.\(OutputImageExtension.png.rawValue)"
),
AssetImageDescription(
idiom: "universal",
scale: "3x",
filename: "\(parsedImage.name)@3x.\(OutputImageExtension.png.rawValue)"
)
],
info: AssetInfo(
version: 1,
author: "ResgenSwift-Imagium"
)
)
debugPrint(property)
debugPrint(expect)
XCTAssertEqual(property, expect)
} }
} }

View File

@ -100,6 +100,64 @@ final class DefinitionTests: XCTestCase {
let expectFr = """ let expectFr = """
/// Translation in fr : /// Translation in fr :
/// C'est la traduction francaise /// C'est la traduction francaise
///
/// Comment :
/// This is a comment
var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "C'est la traduction francaise", comment: "This is a comment")
}
"""
let expectEn = """
/// Translation in en :
/// This is the english translation
///
/// Comment :
/// This is a comment
var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english translation", comment: "This is a comment")
}
"""
let expectEnUs = """
/// Translation in en-us :
/// This is the english us translation
///
/// Comment :
/// This is a comment
var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english us translation", comment: "This is a comment")
}
"""
XCTAssertEqual(propertyFr.adaptForXCTest(), expectFr.adaptForXCTest())
XCTAssertEqual(propertyEn.adaptForXCTest(), expectEn.adaptForXCTest())
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
}
func testGeneratedNSLocalizedStringPropertyWithEmptyComment() {
// Given
let definition = Definition(name: "definition_name")
definition.tags = ["ios","iosonly","notranslation"]
definition.comment = ""
definition.translations = [
"fr": "C'est la traduction francaise",
"en": "This is the english translation",
"en-us": "This is the english us translation"
]
// When
let propertyFr = definition.getNSLocalizedStringProperty(forLang: "fr")
let propertyEn = definition.getNSLocalizedStringProperty(forLang: "en")
let propertyEnUs = definition.getNSLocalizedStringProperty(forLang: "en-us")
// Expect
let expectFr = """
/// Translation in fr :
/// C'est la traduction francaise
///
/// Comment :
/// No comment
var definition_name: String { var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "C'est la traduction francaise", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "C'est la traduction francaise", comment: "")
} }
@ -108,6 +166,9 @@ final class DefinitionTests: XCTestCase {
let expectEn = """ let expectEn = """
/// Translation in en : /// Translation in en :
/// This is the english translation /// This is the english translation
///
/// Comment :
/// No comment
var definition_name: String { var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english translation", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english translation", comment: "")
} }
@ -116,6 +177,9 @@ final class DefinitionTests: XCTestCase {
let expectEnUs = """ let expectEnUs = """
/// Translation in en-us : /// Translation in en-us :
/// This is the english us translation /// This is the english us translation
///
/// Comment :
/// No comment
var definition_name: String { var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english us translation", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english us translation", comment: "")
} }
@ -126,6 +190,62 @@ final class DefinitionTests: XCTestCase {
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest()) XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
} }
func testGeneratedNSLocalizedStringPropertyWithNoComment() {
// Given
let definition = Definition(name: "definition_name")
definition.tags = ["ios","iosonly","notranslation"]
definition.translations = [
"fr": "C'est la traduction francaise",
"en": "This is the english translation",
"en-us": "This is the english us translation"
]
// When
let propertyFr = definition.getNSLocalizedStringProperty(forLang: "fr")
let propertyEn = definition.getNSLocalizedStringProperty(forLang: "en")
let propertyEnUs = definition.getNSLocalizedStringProperty(forLang: "en-us")
// Expect
let expectFr = """
/// Translation in fr :
/// C'est la traduction francaise
///
/// Comment :
/// No comment
var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "C'est la traduction francaise", comment: "")
}
"""
let expectEn = """
/// Translation in en :
/// This is the english translation
///
/// Comment :
/// No comment
var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english translation", comment: "")
}
"""
let expectEnUs = """
/// Translation in en-us :
/// This is the english us translation
///
/// Comment :
/// No comment
var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english us translation", comment: "")
}
"""
XCTAssertEqual(propertyFr.adaptForXCTest(), expectFr.adaptForXCTest())
XCTAssertEqual(propertyEn.adaptForXCTest(), expectEn.adaptForXCTest())
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
}
// MARK: - getNSLocalizedStringStaticProperty
func testGeneratedNSLocalizedStringStaticProperty() { func testGeneratedNSLocalizedStringStaticProperty() {
// Given // Given
let definition = Definition(name: "definition_name") let definition = Definition(name: "definition_name")
@ -146,6 +266,64 @@ final class DefinitionTests: XCTestCase {
let expectFr = """ let expectFr = """
/// Translation in fr : /// Translation in fr :
/// C'est la traduction francaise /// C'est la traduction francaise
///
/// Comment :
/// This is a comment
static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "C'est la traduction francaise", comment: "This is a comment")
}
"""
let expectEn = """
/// Translation in en :
/// This is the english translation
///
/// Comment :
/// This is a comment
static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english translation", comment: "This is a comment")
}
"""
let expectEnUs = """
/// Translation in en-us :
/// This is the english us translation
///
/// Comment :
/// This is a comment
static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english us translation", comment: "This is a comment")
}
"""
XCTAssertEqual(propertyFr.adaptForXCTest(), expectFr.adaptForXCTest())
XCTAssertEqual(propertyEn.adaptForXCTest(), expectEn.adaptForXCTest())
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
}
func testGeneratedNSLocalizedStringStaticPropertyWithEmptyComment() {
// Given
let definition = Definition(name: "definition_name")
definition.tags = ["ios","iosonly","notranslation"]
definition.comment = ""
definition.translations = [
"fr": "C'est la traduction francaise",
"en": "This is the english translation",
"en-us": "This is the english us translation"
]
// When
let propertyFr = definition.getNSLocalizedStringStaticProperty(forLang: "fr")
let propertyEn = definition.getNSLocalizedStringStaticProperty(forLang: "en")
let propertyEnUs = definition.getNSLocalizedStringStaticProperty(forLang: "en-us")
// Expect
let expectFr = """
/// Translation in fr :
/// C'est la traduction francaise
///
/// Comment :
/// No comment
static var definition_name: String { static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "C'est la traduction francaise", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "C'est la traduction francaise", comment: "")
} }
@ -154,6 +332,9 @@ final class DefinitionTests: XCTestCase {
let expectEn = """ let expectEn = """
/// Translation in en : /// Translation in en :
/// This is the english translation /// This is the english translation
///
/// Comment :
/// No comment
static var definition_name: String { static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english translation", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english translation", comment: "")
} }
@ -162,6 +343,63 @@ final class DefinitionTests: XCTestCase {
let expectEnUs = """ let expectEnUs = """
/// Translation in en-us : /// Translation in en-us :
/// This is the english us translation /// This is the english us translation
///
/// Comment :
/// No comment
static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english us translation", comment: "")
}
"""
XCTAssertEqual(propertyFr.adaptForXCTest(), expectFr.adaptForXCTest())
XCTAssertEqual(propertyEn.adaptForXCTest(), expectEn.adaptForXCTest())
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
}
func testGeneratedNSLocalizedStringStaticPropertyWithNoComment() {
// Given
let definition = Definition(name: "definition_name")
definition.tags = ["ios","iosonly","notranslation"]
definition.translations = [
"fr": "C'est la traduction francaise",
"en": "This is the english translation",
"en-us": "This is the english us translation"
]
// When
let propertyFr = definition.getNSLocalizedStringStaticProperty(forLang: "fr")
let propertyEn = definition.getNSLocalizedStringStaticProperty(forLang: "en")
let propertyEnUs = definition.getNSLocalizedStringStaticProperty(forLang: "en-us")
// Expect
let expectFr = """
/// Translation in fr :
/// C'est la traduction francaise
///
/// Comment :
/// No comment
static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "C'est la traduction francaise", comment: "")
}
"""
let expectEn = """
/// Translation in en :
/// This is the english translation
///
/// Comment :
/// No comment
static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english translation", comment: "")
}
"""
let expectEnUs = """
/// Translation in en-us :
/// This is the english us translation
///
/// Comment :
/// No comment
static var definition_name: String { static var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english us translation", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "This is the english us translation", comment: "")
} }
@ -188,12 +426,18 @@ final class DefinitionTests: XCTestCase {
let expectFr = """ let expectFr = """
/// Translation in fr : /// Translation in fr :
/// Welcome "%@" ! /// Welcome "%@" !
///
/// Comment :
/// This is a comment
var definition_name: String { var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "This is a comment")
} }
/// Translation in fr : /// Translation in fr :
/// Welcome "%@" ! /// Welcome "%@" !
///
/// Comment :
/// This is a comment
func definition_name(arg0: String) -> String { func definition_name(arg0: String) -> String {
String(format: self.definition_name, arg0) String(format: self.definition_name, arg0)
} }
@ -218,12 +462,18 @@ final class DefinitionTests: XCTestCase {
let expectFr = """ let expectFr = """
/// Translation in fr : /// Translation in fr :
/// Welcome "%@" ! Your age is %d :) Your weight is %f ;-) /// Welcome "%@" ! Your age is %d :) Your weight is %f ;-)
///
/// Comment :
/// This is a comment
var definition_name: String { var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" ! Your age is %d :) Your weight is %f ;-)", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" ! Your age is %d :) Your weight is %f ;-)", comment: "This is a comment")
} }
/// Translation in fr : /// Translation in fr :
/// Welcome "%@" ! Your age is %d :) Your weight is %f ;-) /// Welcome "%@" ! Your age is %d :) Your weight is %f ;-)
///
/// Comment :
/// This is a comment
func definition_name(arg0: String, arg1: Int, arg2: Double) -> String { func definition_name(arg0: String, arg1: Int, arg2: Double) -> String {
String(format: self.definition_name, arg0, arg1, arg2) String(format: self.definition_name, arg0, arg1, arg2)
} }
@ -249,12 +499,18 @@ final class DefinitionTests: XCTestCase {
let expectFr = """ let expectFr = """
/// Translation in fr : /// Translation in fr :
/// Vous %%: %1$@ %2$@ Age: %3$d /// Vous %%: %1$@ %2$@ Age: %3$d
///
/// Comment :
/// This is a comment
var definition_name: String { var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "Vous %%: %1$@ %2$@ Age: %3$d", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "Vous %%: %1$@ %2$@ Age: %3$d", comment: "This is a comment")
} }
/// Translation in fr : /// Translation in fr :
/// Vous %%: %1$@ %2$@ Age: %3$d /// Vous %%: %1$@ %2$@ Age: %3$d
///
/// Comment :
/// This is a comment
func definition_name(arg0: String, arg1: String, arg2: Int) -> String { func definition_name(arg0: String, arg1: String, arg2: Int) -> String {
String(format: self.definition_name, arg0, arg1, arg2) String(format: self.definition_name, arg0, arg1, arg2)
} }
@ -263,12 +519,18 @@ final class DefinitionTests: XCTestCase {
let expectEn = """ let expectEn = """
/// Translation in en : /// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d /// You %%: %2$@ %1$@ Age: %3$d
///
/// Comment :
/// This is a comment
var definition_name: String { var definition_name: String {
NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "You %%: %2$@ %1$@ Age: %3$d", comment: "") NSLocalizedString("definition_name", tableName: kStringsFileName, bundle: Bundle.main, value: "You %%: %2$@ %1$@ Age: %3$d", comment: "This is a comment")
} }
/// Translation in en : /// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d /// You %%: %2$@ %1$@ Age: %3$d
///
/// Comment :
/// This is a comment
func definition_name(arg0: String, arg1: String, arg2: Int) -> String { func definition_name(arg0: String, arg1: String, arg2: Int) -> String {
String(format: self.definition_name, arg0, arg1, arg2) String(format: self.definition_name, arg0, arg1, arg2)
} }
@ -300,6 +562,9 @@ final class DefinitionTests: XCTestCase {
let expectFr = """ let expectFr = """
/// Translation in fr : /// Translation in fr :
/// C'est la traduction francaise /// C'est la traduction francaise
///
/// Comment :
/// This is a comment
var definition_name: String { var definition_name: String {
"C'est la traduction francaise" "C'est la traduction francaise"
} }
@ -308,6 +573,9 @@ final class DefinitionTests: XCTestCase {
let expectEn = """ let expectEn = """
/// Translation in en : /// Translation in en :
/// This is the english translation /// This is the english translation
///
/// Comment :
/// This is a comment
var definition_name: String { var definition_name: String {
"This is the english translation" "This is the english translation"
} }
@ -316,6 +584,9 @@ final class DefinitionTests: XCTestCase {
let expectEnUs = """ let expectEnUs = """
/// Translation in en-us : /// Translation in en-us :
/// This is the english us translation /// This is the english us translation
///
/// Comment :
/// This is a comment
var definition_name: String { var definition_name: String {
"This is the english us translation" "This is the english us translation"
} }
@ -326,6 +597,117 @@ final class DefinitionTests: XCTestCase {
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest()) XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
} }
func testGeneratedRawPropertyWithEmptyComment() {
// Given
let definition = Definition(name: "definition_name")
definition.tags = ["ios","iosonly","notranslation"]
definition.comment = ""
definition.translations = [
"fr": "C'est la traduction francaise",
"en": "This is the english translation",
"en-us": "This is the english us translation"
]
// When
let propertyFr = definition.getProperty(forLang: "fr")
let propertyEn = definition.getProperty(forLang: "en")
let propertyEnUs = definition.getProperty(forLang: "en-us")
// Expect
let expectFr = """
/// Translation in fr :
/// C'est la traduction francaise
///
/// Comment :
/// No comment
var definition_name: String {
"C'est la traduction francaise"
}
"""
let expectEn = """
/// Translation in en :
/// This is the english translation
///
/// Comment :
/// No comment
var definition_name: String {
"This is the english translation"
}
"""
let expectEnUs = """
/// Translation in en-us :
/// This is the english us translation
///
/// Comment :
/// No comment
var definition_name: String {
"This is the english us translation"
}
"""
XCTAssertEqual(propertyFr.adaptForXCTest(), expectFr.adaptForXCTest())
XCTAssertEqual(propertyEn.adaptForXCTest(), expectEn.adaptForXCTest())
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
}
func testGeneratedRawPropertyWithNoComment() {
// Given
let definition = Definition(name: "definition_name")
definition.tags = ["ios","iosonly","notranslation"]
definition.translations = [
"fr": "C'est la traduction francaise",
"en": "This is the english translation",
"en-us": "This is the english us translation"
]
// When
let propertyFr = definition.getProperty(forLang: "fr")
let propertyEn = definition.getProperty(forLang: "en")
let propertyEnUs = definition.getProperty(forLang: "en-us")
// Expect
let expectFr = """
/// Translation in fr :
/// C'est la traduction francaise
///
/// Comment :
/// No comment
var definition_name: String {
"C'est la traduction francaise"
}
"""
let expectEn = """
/// Translation in en :
/// This is the english translation
///
/// Comment :
/// No comment
var definition_name: String {
"This is the english translation"
}
"""
let expectEnUs = """
/// Translation in en-us :
/// This is the english us translation
///
/// Comment :
/// No comment
var definition_name: String {
"This is the english us translation"
}
"""
XCTAssertEqual(propertyFr.adaptForXCTest(), expectFr.adaptForXCTest())
XCTAssertEqual(propertyEn.adaptForXCTest(), expectEn.adaptForXCTest())
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
}
// MARK: - Raw static properties
func testGeneratedRawStaticProperty() { func testGeneratedRawStaticProperty() {
// Given // Given
let definition = Definition(name: "definition_name") let definition = Definition(name: "definition_name")
@ -346,6 +728,9 @@ final class DefinitionTests: XCTestCase {
let expectFr = """ let expectFr = """
/// Translation in fr : /// Translation in fr :
/// C'est la traduction francaise /// C'est la traduction francaise
///
/// Comment :
/// This is a comment
static var definition_name: String { static var definition_name: String {
"C'est la traduction francaise" "C'est la traduction francaise"
} }
@ -354,6 +739,9 @@ final class DefinitionTests: XCTestCase {
let expectEn = """ let expectEn = """
/// Translation in en : /// Translation in en :
/// This is the english translation /// This is the english translation
///
/// Comment :
/// This is a comment
static var definition_name: String { static var definition_name: String {
"This is the english translation" "This is the english translation"
} }
@ -362,6 +750,118 @@ final class DefinitionTests: XCTestCase {
let expectEnUs = """ let expectEnUs = """
/// Translation in en-us : /// Translation in en-us :
/// This is the english us translation /// This is the english us translation
///
/// Comment :
/// This is a comment
static var definition_name: String {
"This is the english us translation"
}
"""
XCTAssertEqual(propertyFr.adaptForXCTest(), expectFr.adaptForXCTest())
XCTAssertEqual(propertyEn.adaptForXCTest(), expectEn.adaptForXCTest())
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
}
func testGeneratedRawStaticPropertyWithEmptyComment() {
// Given
let definition = Definition(name: "definition_name")
definition.tags = ["ios","iosonly","notranslation"]
definition.comment = ""
definition.translations = [
"fr": "C'est la traduction francaise",
"en": "This is the english translation",
"en-us": "This is the english us translation"
]
// When
let propertyFr = definition.getStaticProperty(forLang: "fr")
let propertyEn = definition.getStaticProperty(forLang: "en")
let propertyEnUs = definition.getStaticProperty(forLang: "en-us")
// Expect
let expectFr = """
/// Translation in fr :
/// C'est la traduction francaise
///
/// Comment :
/// No comment
static var definition_name: String {
"C'est la traduction francaise"
}
"""
let expectEn = """
/// Translation in en :
/// This is the english translation
///
/// Comment :
/// No comment
static var definition_name: String {
"This is the english translation"
}
"""
let expectEnUs = """
/// Translation in en-us :
/// This is the english us translation
///
/// Comment :
/// No comment
static var definition_name: String {
"This is the english us translation"
}
"""
XCTAssertEqual(propertyFr.adaptForXCTest(), expectFr.adaptForXCTest())
XCTAssertEqual(propertyEn.adaptForXCTest(), expectEn.adaptForXCTest())
XCTAssertEqual(propertyEnUs.adaptForXCTest(), expectEnUs.adaptForXCTest())
}
func testGeneratedRawStaticPropertyWithNoComment() {
// Given
let definition = Definition(name: "definition_name")
definition.tags = ["ios","iosonly","notranslation"]
definition.translations = [
"fr": "C'est la traduction francaise",
"en": "This is the english translation",
"en-us": "This is the english us translation"
]
// When
let propertyFr = definition.getStaticProperty(forLang: "fr")
let propertyEn = definition.getStaticProperty(forLang: "en")
let propertyEnUs = definition.getStaticProperty(forLang: "en-us")
// Expect
let expectFr = """
/// Translation in fr :
/// C'est la traduction francaise
///
/// Comment :
/// No comment
static var definition_name: String {
"C'est la traduction francaise"
}
"""
let expectEn = """
/// Translation in en :
/// This is the english translation
///
/// Comment :
/// No comment
static var definition_name: String {
"This is the english translation"
}
"""
let expectEnUs = """
/// Translation in en-us :
/// This is the english us translation
///
/// Comment :
/// No comment
static var definition_name: String { static var definition_name: String {
"This is the english us translation" "This is the english us translation"
} }

File diff suppressed because it is too large Load Diff

48
script/swiftlint.sh Executable file
View File

@ -0,0 +1,48 @@
#!/bin/sh
# Go to git repo root level
cd $(git rev-parse --show-toplevel)
if [[ "$BUILD_DIR" == *"IBDesignables"* ]] || [[ "$BUILD_DIR" == *"Previews"* ]] ; then
echo "not linting for IBDesignables/SwiftUI Previews builds";
exit 0
fi
SWIFT_LINT=$(which swiftlint)
if [[ -z $SWIFT_LINT ]] ; then
echo "warning: SwiftLint not installed, please download it from https://github.com/realm/SwiftLint"
exit 0
fi
if [[ $RUN_CLANG_STATIC_ANALYZER == "YES" ]] ; then
time $SWIFT_LINT
else
COUNT=0
##### Check for modified git files #####
FILES=$(git diff --name-only | grep -iv "^carthage" | grep -iv "^pods" | grep -iv "^vendor" | grep -v "R2" | grep ".swift$")
if [ ! -z "$FILES" ]; then
while read FILE_PATH; do
export SCRIPT_INPUT_FILE_$COUNT=$FILE_PATH
COUNT=$((COUNT + 1))
done <<< "$FILES"
fi
##### Check for modified files in unstaged/Staged area #####
FILES=$(git diff --name-only --cached --diff-filter=d | grep -iv "^carthage" | grep -iv "^pods" | grep -iv "^vendor" | grep -v "R2" | grep ".swift$")
if [ ! -z "$FILES" ]; then
while read FILE_PATH; do
export SCRIPT_INPUT_FILE_$COUNT=$FILE_PATH
COUNT=$((COUNT + 1))
done <<< "$FILES"
fi
##### Make the count avilable as global variable #####
export SCRIPT_INPUT_FILE_COUNT=$COUNT
env | grep SCRIPT_INPUT_FILE_
if [[ COUNT -ne 0 ]] ; then
time $SWIFT_LINT --use-script-input-files
fi
fi