From 0cc74a3160deedbef8f412fc8e323c1580562a1a Mon Sep 17 00:00:00 2001 From: youssefsiam38 Date: Fri, 9 Aug 2024 03:22:09 +0300 Subject: [PATCH] init --- .gitignore | 2 + README.md | 99 +++++++++ cmd/init.go | 22 ++ cmd/root.go | 138 ++++++++++++ cmd/version.go | 20 ++ go.mod | 33 +++ go.sum | 81 +++++++ main.go | 9 + utils/utils.go | 557 +++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 961 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/init.go create mode 100644 cmd/root.go create mode 100644 cmd/version.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 utils/utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5d85cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dictionaries +.fasi7* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..72826a8 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Fasi7 + +## Overview + +Fasi7 is a command-line interface (CLI) tool designed to help developers translate their localization files using AI. It automates the process of translating content from one language to multiple target languages, preserving placeholders and adapting to the context of your business. + +## Installation + +Download the latest release from the [releases page](https://github.com/youssefsiam38/fasi7/releases) + +## Configuration + +Fasi7 uses a configuration file to store settings. By default, it looks for a file named `.fasi7.yaml` in the current directory. + +### Creating a Configuration File + +To create a new configuration file, run: + +``` +fasi7 init +``` + +### Configuration Options + +The following options can be set in the configuration file: + +- `openai.apiKey`: Your OpenAI API key +- `openai.model`: The OpenAI model to use for translations +- `openai.systemPrompt`: Custom system prompt for the AI (optional) +- `outputLocales`: List of target locales for translation +- `inputLocale`: The source locale to translate from +- `businessDescription`: A description of your business to provide context for translations +- `dir`: The directory containing localization files +- `ignoreFilesWithContent`: (Optional) If specified, files containing this string will be ignored + +## Usage + +To run Fasi7 and translate your localization files: + +``` +fasi7 +``` + +This command will: + +1. Read the configuration file +2. Recursively search for files in the specified directory +3. Identify files matching the input locale +4. Translate the content to each specified output locale +5. Write the translated content to new files, preserving the directory structure + +## Command-line Options + +- `--config`: Specify a custom path for the configuration file (default: `.fasi7.yaml`) + +Example: +``` +fasi7 --config /path/to/custom/config.yaml +``` + +## How It Works + +1. Fasi7 uses the OpenAI API to perform translations. +2. It sends each file's content to the AI along with a system prompt that includes: + - The source and target languages + - Instructions to preserve placeholders + - Your business description for context +3. The AI generates a translation, which is then saved to a new file in the appropriate locale directory. + +## Error Handling + +Fasi7 will exit with an error message in the following cases: + +- No configuration file found +- Missing required configuration options +- File read/write errors +- OpenAI API errors + +## Concurrent Processing + +Fasi7 translates files concurrently to improve performance when dealing with multiple output locales or many files. + +## Best Practices + +1. Provide a detailed business description in your configuration to improve translation accuracy and context. +2. Use meaningful placeholder names in your localization files to ensure they are correctly preserved. +3. Choose an appropriate model for your needs. + +## Limitations + +- Fasi7 relies on the OpenAI API, which may have usage limits and costs associated with it. +- The quality of translations depends on the AI model used and the context provided. + +## Contributing + +Contributions are welcome! Please submit issues or pull requests to help improve Fasi7. + +## License +MIT License \ No newline at end of file diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..86576ce --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/youssefsiam38/fasi7/utils" +) + +func init() { + rootCmd.AddCommand(initCmd) +} + +var initCmd = &cobra.Command{ + Use: "init", + Short: "Initialize Fasi7", + Long: `Initialize Fasi7 by creating a config file`, + Run: func(cmd *cobra.Command, args []string) { + err := utils.WriteFile(".fasi7.yaml", []byte("dir: ./locales # Required\r\nbusinessDescription: | # This will be ignored if you specify a systemPrompt\r\n \r\nopenai:\r\n apiKey: ${OPENAI_API_KEY} # Required\r\n model: gpt-4o-mini # Required\r\n # systemPrompt: | # Optional\r\ninputLocale: en # Required\r\noutputLocales: # Required\r\n - ar\r\n - de\r\n - es\r\n - fr\r\n - ru\r\nignoreFilesWithContent: '{}' # Optional")) + if err != nil { + panic(err) + } + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..e7ac1b3 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,138 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + "sync" + + "github.com/sashabaranov/go-openai" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/youssefsiam38/fasi7/utils" +) + +var cfgFile string +var noConfig bool + +func init() { + cobra.OnInitialize(initConfig) + // config + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", ".fasi7.yaml", "config file path") +} + +func initConfig() { + viper.SetConfigFile(cfgFile) + if err := viper.ReadInConfig(); err != nil { + noConfig = true + } +} + +var rootCmd = &cobra.Command{ + Use: "fasi7", + Short: "Fasi7 is a localization files translation tool using AI", + Long: `Fasi7 was made to help developers to translate their localization files using AI. for more information visit github.com/youssefsiam38/fasi7`, + Run: func(cmd *cobra.Command, args []string) { + if noConfig { + fmt.Println("No config file found, run 'fasi7 init' to create one") + os.Exit(1) + } + ctx := cmd.Context() + openaiClient := openai.NewClient(utils.GetConfigString("openai.apiKey")) + outputLocales := viper.GetStringSlice("outputLocales") + if len(outputLocales) == 0 { + fmt.Println("No output locales found in config file") + os.Exit(1) + } + + inputLocale := utils.GetConfigString("inputLocale") + if inputLocale == "" { + fmt.Println("No input locale found in config file") + os.Exit(1) + } + + businessDescription := utils.GetConfigString("businessDescription") + if businessDescription == "" { + fmt.Println("No business description found in config file") + os.Exit(1) + } + + files, err := utils.GetFilesRecursive(utils.GetConfigString("dir")) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + wg := sync.WaitGroup{} + + for _, file := range files { + if !strings.Contains(file, inputLocale) { + continue + } + + fileContent, err := os.ReadFile(file) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + if utils.GetConfigString("ignoreFilesWithContent") != "" && strings.Contains(string(fileContent), utils.GetConfigString("ignoreFilesWithContent")) { + continue + } + + for _, outputLocale := range outputLocales { + wg.Add(1) + go func(fileContent []byte, file, outputLocale string) { + defer wg.Done() + systemPrompt := utils.GetConfigString("openai.systemPrompt") + if systemPrompt == "" { + systemPrompt = fmt.Sprintf(` + You are an expert language translator. Your task is to translate the text provided to you from %s to %s, while preserving any placeholders that may be present in the source text. + +The input text will be the original file content, and the output text should be parsable content, do not include any comments or unnecessary whitespace. + +If the input text contains any placeholders (e.g. {name}, {count}, etc.), ensure that these placeholders remain unchanged in the Arabic translation. + +Provide a high-quality translation that conveys the original meaning accurately and idiomatically. Do not simply perform a literal word-for-word translation, but adapt the phrasing and grammar to produce natural-sounding text that is appropriate for the target audience for the business described below. + +Business Description: +%s + `, utils.IsoToLanguage(inputLocale), utils.IsoToLanguage(outputLocale), businessDescription) + } + + res, err := openaiClient.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ + Model: utils.GetConfigString("openai.model"), + Messages: []openai.ChatCompletionMessage{ + { + Role: "system", + Content: systemPrompt, + }, + { + Role: "user", + Content: string(fileContent), + }, + }, + }) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + err = utils.WriteFile(strings.ReplaceAll(file, fmt.Sprintf("/%s/", inputLocale), fmt.Sprintf("/%s/", outputLocale)), []byte(res.Choices[0].Message.Content)) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + }(fileContent, file, outputLocale) + } + } + + wg.Wait() + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..68a0ada --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(versionCmd) +} + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of Fasi7", + Long: `All software has versions. This is Fasi7's`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Fasi7 v0.1") + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..095670a --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module github.com/youssefsiam38/fasi7 + +go 1.22.0 + +require ( + github.com/samber/lo v1.46.0 + github.com/sashabaranov/go-openai v1.28.1 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..412b5df --- /dev/null +++ b/go.sum @@ -0,0 +1,81 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= +github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/sashabaranov/go-openai v1.28.1 h1:aREx6faUTeOZNMDTNGAY8B9vNmmN7qoGvDV0Ke2J1Mc= +github.com/sashabaranov/go-openai v1.28.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d02fb78 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/youssefsiam38/fasi7/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..0fa2c44 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,557 @@ +package utils + +import ( + "bufio" + "os" + "path/filepath" + "strings" + + "github.com/samber/lo" + "github.com/spf13/viper" +) + +func GetConfigString(key string) string { + return os.ExpandEnv(viper.GetString(key)) +} + +func GetFilesRecursive(dir string) ([]string, error) { + var files []string + fasi7ignore, err := parseFasi7ignore(dir) + if err != nil { + return nil, err + } + + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + relPath, err := filepath.Rel(dir, path) + if err != nil { + return err + } + + if lo.Contains(fasi7ignore, relPath) { + return nil + } + + files = append(files, path) + return nil + }) + + // exlcude .fasi7ignore file + for i, file := range files { + if strings.HasSuffix(file, ".fasi7ignore") { + files = append(files[:i], files[i+1:]...) + break + } + } + + return files, err +} + +func parseFasi7ignore(dir string) ([]string, error) { + fasi7ignorePath := filepath.Join(dir, ".fasi7ignore") + data, err := os.ReadFile(fasi7ignorePath) + if err != nil { + if os.IsNotExist(err) { + return []string{}, nil + } + return nil, err + } + + var fasi7ignore []string + scanner := bufio.NewScanner(strings.NewReader(string(data))) + for scanner.Scan() { + line := scanner.Text() + if line == "" || strings.HasPrefix(line, "#") { + continue + } + fasi7ignore = append(fasi7ignore, line) + } + + return fasi7ignore, scanner.Err() +} + +func WriteFile(path string, data []byte) error { + // Create all the parent directories + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + // Write the file + if err := os.WriteFile(path, data, 0644); err != nil { + return err + } + + return nil +} + +func IsoToLanguage(iso string) string { + switch iso { + case "af": + return "Afrikaans" + case "af-ZA": + return "Afrikaans (South Africa)" + case "ar": + return "Arabic" + case "ar-AE": + return "Arabic (U.A.E.)" + case "ar-BH": + return "Arabic (Bahrain)" + case "ar-DZ": + return "Arabic (Algeria)" + case "ar-EG": + return "Arabic (Egypt)" + case "ar-IQ": + return "Arabic (Iraq)" + case "ar-JO": + return "Arabic (Jordan)" + case "ar-KW": + return "Arabic (Kuwait)" + case "ar-LB": + return "Arabic (Lebanon)" + case "ar-LY": + return "Arabic (Libya)" + case "ar-MA": + return "Arabic (Morocco)" + case "ar-OM": + return "Arabic (Oman)" + case "ar-QA": + return "Arabic (Qatar)" + case "ar-SA": + return "Arabic (Saudi Arabia)" + case "ar-SY": + return "Arabic (Syria)" + case "ar-TN": + return "Arabic (Tunisia)" + case "ar-YE": + return "Arabic (Yemen)" + case "az": + return "Azeri (Latin)" + case "az-AZ": + return "Azeri (Latin) (Azerbaijan)" + case "be": + return "Belarusian" + case "be-BY": + return "Belarusian (Belarus)" + case "bg": + return "Bulgarian" + case "bg-BG": + return "Bulgarian (Bulgaria)" + case "bs-BA": + return "Bosnian (Bosnia and Herzegovina)" + case "ca": + return "Catalan" + case "ca-ES": + return "Catalan (Spain)" + case "cs": + return "Czech" + case "cs-CZ": + return "Czech (Czech Republic)" + case "cy": + return "Welsh" + case "cy-GB": + return "Welsh (United Kingdom)" + case "da": + return "Danish" + case "da-DK": + return "Danish (Denmark)" + case "de": + return "German" + case "de-AT": + return "German (Austria)" + case "de-CH": + return "German (Switzerland)" + case "de-DE": + return "German (Germany)" + case "de-LI": + return "German (Liechtenstein)" + case "de-LU": + return "German (Luxembourg)" + case "dv": + return "Divehi" + case "dv-MV": + return "Divehi (Maldives)" + case "el": + return "Greek" + case "el-GR": + return "Greek (Greece)" + case "en": + return "English" + case "en-AU": + return "English (Australia)" + case "en-BZ": + return "English (Belize)" + case "en-CA": + return "English (Canada)" + case "en-CB": + return "English (Caribbean)" + case "en-GB": + return "English (United Kingdom)" + case "en-IE": + return "English (Ireland)" + case "en-JM": + return "English (Jamaica)" + case "en-NZ": + return "English (New Zealand)" + case "en-PH": + return "English (Republic of the Philippines)" + case "en-TT": + return "English (Trinidad and Tobago)" + case "en-US": + return "English (United States)" + case "en-ZA": + return "English (South Africa)" + case "en-ZW": + return "English (Zimbabwe)" + case "eo": + return "Esperanto" + case "es": + return "Spanish" + case "es-AR": + return "Spanish (Argentina)" + case "es-BO": + return "Spanish (Bolivia)" + case "es-CL": + return "Spanish (Chile)" + case "es-CO": + return "Spanish (Colombia)" + case "es-CR": + return "Spanish (Costa Rica)" + case "es-DO": + return "Spanish (Dominican Republic)" + case "es-EC": + return "Spanish (Ecuador)" + case "es-ES": + return "Spanish (Spain)" + case "es-GT": + return "Spanish (Guatemala)" + case "es-HN": + return "Spanish (Honduras)" + case "es-MX": + return "Spanish (Mexico)" + case "es-NI": + return "Spanish (Nicaragua)" + case "es-PA": + return "Spanish (Panama)" + case "es-PE": + return "Spanish (Peru)" + case "es-PR": + return "Spanish (Puerto Rico)" + case "es-PY": + return "Spanish (Paraguay)" + case "es-SV": + return "Spanish (El Salvador)" + case "es-UY": + return "Spanish (Uruguay)" + case "es-VE": + return "Spanish (Venezuela)" + case "et": + return "Estonian" + case "et-EE": + return "Estonian (Estonia)" + case "eu": + return "Basque" + case "eu-ES": + return "Basque (Spain)" + case "fa": + return "Farsi" + case "fa-IR": + return "Farsi (Iran)" + case "fi": + return "Finnish" + case "fi-FI": + return "Finnish (Finland)" + case "fo": + return "Faroese" + case "fo-FO": + return "Faroese (Faroe Islands)" + case "fr": + return "French" + case "fr-BE": + return "French (Belgium)" + case "fr-CA": + return "French (Canada)" + case "fr-CH": + return "French (Switzerland)" + case "fr-FR": + return "French (France)" + case "fr-LU": + return "French (Luxembourg)" + case "fr-MC": + return "French (Principality of Monaco)" + case "gl": + return "Galician" + case "gl-ES": + return "Galician (Spain)" + case "gu": + return "Gujarati" + case "gu-IN": + return "Gujarati (India)" + case "he": + return "Hebrew" + case "he-IL": + return "Hebrew" + case "hi": + return "Hindi" + case "hi-IN": + return "Hindi (India)" + case "hr": + return "Croatian" + case "hr-BA": + return "Croatian (Bosnia and Herzegovina)" + case "hr-HR": + return "Croatian (Croatia)" + case "hu": + return "Hungarian" + case "hu-HU": + return "Hungarian (Hungary)" + case "hy": + return "Armenian" + case "hy-AM": + return "Armenian (Armenia)" + case "id": + return "Indonesian" + case "id-ID": + return "Indonesian (Indonesia)" + case "is": + return "Icelandic" + case "is-IS": + return "Icelandic (Iceland)" + case "it": + return "Italian" + case "it-CH": + return "Italian (Switzerland)" + case "it-IT": + return "Italian (Italy)" + case "ja": + return "Japanese" + case "ja-JP": + return "Japanese (Japan)" + case "ka": + return "Georgian" + case "ka-GE": + return "Georgian (Georgia)" + case "kk": + return "Kazakh" + case "kk-KZ": + return "Kazakh (Kazakhstan)" + case "kn": + return "Kannada" + case "kn-IN": + return "Kannada (India)" + case "ko": + return "Korean" + case "ko-KR": + return "Korean (Korea)" + case "kok": + return "Konkani" + case "kok-IN": + return "Konkani (India)" + case "ky": + return "Kyrgyz" + case "ky-KG": + return "Kyrgyz (Kyrgyzstan)" + case "lt": + return "Lithuanian" + case "lt-LT": + return "Lithuanian (Lithuania)" + case "lv": + return "Latvian" + case "lv-LV": + return "Latvian (Latvia)" + case "mi": + return "Maori" + case "mi-NZ": + return "Maori (New Zealand)" + case "mk": + return "FYRO Macedonian" + case "mk-MK": + return "FYRO Macedonian (Former Yugoslav Republic of Macedonia)" + case "mn": + return "Mongolian" + case "mn-MN": + return "Mongolian (Mongolia)" + case "mr": + return "Marathi" + case "mr-IN": + return "Marathi (India)" + case "ms": + return "Malay" + case "ms-BN": + return "Malay (Brunei Darussalam)" + case "ms-MY": + return "Malay (Malaysia)" + + case "mt": + return "Maltese" + case "mt-MT": + return "Maltese (Malta)" + case "nb": + return "Norwegian (Bokmål)" + case "nb-NO": + return "Norwegian (Bokmål) (Norway)" + case "nl": + return "Dutch" + case "nl-BE": + return "Dutch (Belgium)" + case "nl-NL": + return "Dutch (Netherlands)" + case "nn-NO": + return "Norwegian (Nynorsk) (Norway)" + case "ns": + return "Northern Sotho" + case "ns-ZA": + return "Northern Sotho (South Africa)" + case "pa": + return "Punjabi" + case "pa-IN": + return "Punjabi (India)" + case "pl": + return "Polish" + case "pl-PL": + return "Polish (Poland)" + case "ps": + return "Pashto" + case "ps-AR": + return "Pashto (Afghanistan)" + case "pt": + return "Portuguese" + case "pt-BR": + return "Portuguese (Brazil)" + case "pt-PT": + return "Portuguese (Portugal)" + case "qu": + return "Quechua" + case "qu-BO": + return "Quechua (Bolivia)" + case "qu-EC": + return "Quechua (Ecuador)" + case "qu-PE": + return "Quechua (Peru)" + case "ro": + return "Romanian" + case "ro-RO": + return "Romanian (Romania)" + case "ru": + return "Russian" + case "ru-RU": + return "Russian (Russia)" + case "sa": + return "Sanskrit" + case "sa-IN": + return "Sanskrit (India)" + case "se": + return "Sami (Northern)" + case "se-FI": + return "Sami (Northern) (Finland)" + case "se-NO": + return "Sami (Northern) (Norway)" + case "se-SE": + return "Sami (Northern) (Sweden)" + case "sk": + return "Slovak" + case "sk-SK": + return "Slovak (Slovakia)" + case "sl": + return "Slovenian" + case "sl-SI": + return "Slovenian (Slovenia)" + case "sq": + return "Albanian" + case "sq-AL": + return "Albanian (Albania)" + case "sr-BA": + return "Serbian (Latin) (Bosnia and Herzegovina)" + case "sr-SP": + return "Serbian (Latin) (Serbia and Montenegro)" + case "sv": + return "Swedish" + case "sv-FI": + return "Swedish (Finland)" + case "sv-SE": + return "Swedish (Sweden)" + case "sw": + return "Swahili" + case "sw-KE": + return "Swahili (Kenya)" + case "syr": + return "Syriac" + case "syr-SY": + return "Syriac (Syria)" + case "ta": + return "Tamil" + case "ta-IN": + return "Tamil (India)" + case "te": + return "Telugu" + case "te-IN": + return "Telugu (India)" + case "th": + return "Thai" + case "th-TH": + return "Thai (Thailand)" + case "tl": + return "Tagalog" + case "tl-PH": + return "Tagalog (Philippines)" + case "tn": + return "Tswana" + case "tn-ZA": + return "Tswana (South Africa)" + case "tr": + return "Turkish" + case "tr-TR": + return "Turkish (Turkey)" + case "tt": + return "Tatar" + case "tt-RU": + return "Tatar (Russia)" + case "ts": + return "Tsonga" + case "uk": + return "Ukrainian" + case "uk-UA": + return "Ukrainian (Ukraine)" + case "ur": + return "Urdu" + case "ur-PK": + return "Urdu (Islamic Republic of Pakistan)" + case "uz": + return "Uzbek (Latin)" + case "uz-UZ": + return "Uzbek (Latin) (Uzbekistan)" + case "vi": + return "Vietnamese" + case "vi-VN": + return "Vietnamese (Viet Nam)" + case "xh": + return "Xhosa" + case "xh-ZA": + return "Xhosa (South Africa)" + case "zh": + return "Chinese" + case "zh-CN": + return "Chinese (S)" + case "zh-HK": + return "Chinese (Hong Kong)" + case "zh-MO": + return "Chinese (Macau)" + case "zh-SG": + return "Chinese (Singapore)" + case "zh-TW": + return "Chinese (T)" + case "zu": + return "Zulu" + case "zu-ZA": + return "Zulu (South Africa)" + default: + return iso + } +}