-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f8db41e
commit 0cc74a3
Showing
9 changed files
with
961 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dictionaries | ||
.fasi7* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <Business description here>\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) | ||
} | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) |
Oops, something went wrong.