Translate your Go program into multiple languages.

Overview

go-i18n Build status Report card codecov Sourcegraph

go-i18n is a Go package and a command that helps you translate Go programs into multiple languages.

Package i18n GoDoc

The i18n package provides support for looking up messages according to a set of locale preferences.

import "github.com/nicksnyder/go-i18n/v2/i18n"

Create a Bundle to use for the lifetime of your application.

bundle := i18n.NewBundle(language.English)

Load translations into your bundle during initialization.

bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.LoadMessageFile("es.toml")

Create a Localizer to use for a set of language preferences.

func(w http.ResponseWriter, r *http.Request) {
    lang := r.FormValue("lang")
    accept := r.Header.Get("Accept-Language")
    localizer := i18n.NewLocalizer(bundle, lang, accept)
}

Use the Localizer to lookup messages.

localizer.Localize(&i18n.LocalizeConfig{
    DefaultMessage: &i18n.Message{
        ID: "PersonCats",
        One: "{{.Name}} has {{.Count}} cat.",
        Other: "{{.Name}} has {{.Count}} cats.",
    },
    TemplateData: map[string]interface{}{
        "Name": "Nick",
        "Count": 2,
    },
    PluralCount: 2,
}) // Nick has 2 cats.

Command goi18n GoDoc

The goi18n command manages message files used by the i18n package.

go get -u github.com/nicksnyder/go-i18n/v2/goi18n
goi18n -help

Extracting messages

Use goi18n extract to extract all i18n.Message struct literals in Go source files to a message file for translation.

# active.en.toml
[PersonCats]
description = "The number of cats a person has"
one = "{{.Name}} has {{.Count}} cat."
other = "{{.Name}} has {{.Count}} cats."

Translating a new language

  1. Create an empty message file for the language that you want to add (e.g. translate.es.toml).

  2. Run goi18n merge active.en.toml translate.es.toml to populate translate.es.toml with the messages to be translated.

    # translate.es.toml
    [HelloPerson]
    hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
    other = "Hello {{.Name}}"
  3. After translate.es.toml has been translated, rename it to active.es.toml.

    # active.es.toml
    [HelloPerson]
    hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
    other = "Hola {{.Name}}"
  4. Load active.es.toml into your bundle.

    bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
    bundle.LoadMessageFile("active.es.toml")

Translating new messages

If you have added new messages to your program:

  1. Run goi18n extract to update active.en.toml with the new messages.
  2. Run goi18n merge active.*.toml to generate updated translate.*.toml files.
  3. Translate all the messages in the translate.*.toml files.
  4. Run goi18n merge active.*.toml translate.*.toml to merge the translated messages into the active message files.

For more information and examples:

License

go-i18n is available under the MIT license. See the LICENSE file for more info.

Comments
  • Release v2.0.0

    Release v2.0.0

    I have started to prototype what v2 of this library might look like.

    The overall goals of v2 are to

    1. Address all issues that are currently open on the project
    2. Apply some Go best practices to the codebase (e.g. remove global state, explicit error returns)
    3. Revisit API design and make breaking changes which are necessary to accomplish (1) and (2).

    My development plan is to work on this in a branch until I am happy with the results. Then, I will merge this into master under a subfolder called v2-beta and tag a 1.x.0 release. Projects can then begin to try out the new API and provide feedback. APIs under v2-beta subfolder will be subject to change pending community feedback. Once I am happy with the results, I will make a final 1.x.0 release that contains the v2 api under the v2 subfolder. ~~Immediately following that, I will delete the v1 package files, move v2-beta to the root of the repository, and tag 2.0.0.~~

    I do not have an estimate for when this work will be complete as I am working on it in spare time here and there.

    If you have general feedback about this process it can be shared here. If there are other things you would want to see in a world where breaking API changes are possible, please create a new issue and I will consider it.

    opened by nicksnyder 18
  • How do you handle l10n ?

    How do you handle l10n ?

    How do you handle the format for dates, numbers, etc.. ?

    How about stealing this: https://github.com/jbnicolai/closure-library/blob/20702eff5afe0fd9d8c5a42254879fc18c34253b/closure/goog/i18n/datetimepatternsext.js ?

    For example, the angular folks leverage it through their https://docs.angularjs.org/api/ng/filter/date filter.. same with number and currency filters..

    enhancement 
    opened by abourget 15
  • Allow use custom language codes

    Allow use custom language codes

    Fix #72 Fix https://github.com/spf13/hugo/issues/3564

    go-i18n doesn't return an error now, if user provides non-registered language code.

    /cc @bep @nicksnyder

    opened by bogem 14
  • bundle: runtime-reflect structs to map[string]interface{}

    bundle: runtime-reflect structs to map[string]interface{}

    Fixes #24.

    @nicksnyder The structs package is quite lightweight. It is also licensed under MIT. I can attempt to extract the Struct.structFields() and Struct.Map() methods if you would prefer.

    opened by parkr 12
  • Odd behaviour with DefaultMessage

    Odd behaviour with DefaultMessage

    Working on https://github.com/gohugoio/hugo/pull/5243 and trying to find a workaround for #124 I found some odd behaviour with DefaultMessage

    translated, err := localizer.Localize(&i18n.LocalizeConfig{
    				//DefaultMessage: defaultMessage,
    				MessageID:    translationID,
    				TemplateData: templateData,
    			})
    

    I have a test case which returns translated as expected and no error with the code above. If I uncomment the DefaultMessage, I always get the default message in return.

    opened by bep 11
  • The command

    The command "goi18n constants" is missing

    Hello! I use "github.com/nicksnyder/go-i18n/v2/i18n" and I want to use the command "goi18n constants", but for some reason it is unavailable in the 2nd version of this package.

    I tried to use the first version of the package to generate constants, but it seems that it is incompatible with the format of the 2nd version:

    goi18n constants es.toml
    failed to load translation file es.toml because unable to parse translation #0 because invalid plural category description
    map[id:PersonCats translation:map[description:The number of cats a person has one:{{.Name}} has {{.Count}} cat. other:{{.Name}} has {{.Count}} cats.]]
    
    # es.toml
    [PersonCats]
    description = "The number of cats a person has"
    one = "{{.Name}} has {{.Count}} cat."
    other = "{{.Name}} has {{.Count}} cats."
    

    Can you explain why this command is not available in the second version of the package and do you have plans to return it?

    question 
    opened by VadimKulagin 10
  • [Feature] Command goi18n generate dynamic struct R.go

    [Feature] Command goi18n generate dynamic struct R.go

    Working with Android/Java the IDE generates a dynamic class R.java with all strings ids, this method avoid a lot of bugs. In a big project is easy to reference an invalid id or to have a lot of unused ids.

    What do you think the command goi18n generates R.go ? Example:

    en-US.all.json

    [
      {
        "id": "settings_title",
        "translation": "Settings"
      }
    ]
    
    $ goi18n -genconst en-US.all.json -outdir ./
    

    this command generates R.go

    package R
    const SettingsTitle string = "settings_title"
    

    In code the new way will be:

    T(R.SettingsTitle)
    

    This is only a suggestion, let me know what do you think. I can help with this change thanks

    enhancement 
    opened by rodcorsi 10
  • Question: {{T

    Question: {{T "messageID" .Param1 .Param2}} in v2

    Hi, just found this package today, played around with v2 a little bit, and it works great. Great package.

    My goal is to use translations from html/template files only, like this:

    {{T "messageID" .Param1 .Param2}}
    

    I see that there isn't a Tfunc() in v2 as opposed to v1 so I wrote a wrapper:

    lang := r.FormValue("lang")
    accept := r.Header.Get("Accept-Language")
    localizer := i18n.NewLocalizer(bundle, lang, accept)
    T := func(messageID string, args ...interface{}) string {
    	lc := i18n.LocalizeConfig{MessageID: messageID}
    	if len(args) > 1 {
    		lc.PluralCount = args[0]
    		td := make(map[string]interface{})
    		td["PluralCount"] = lc.PluralCount
    		for i := 1; i < len(args); i++ {
    			td["P"+strconv.Itoa(i)] = args[i]
    		}
    		lc.TemplateData = td
    	}
    	return localizer.MustLocalize(&lc)
    }
    t.Funcs(map[string]interface{}{
    	"T": T,
    })
    ...
    

    This makes it possbile to have a translation like this:

    [PersonUnreadEmails]
    description = "The number of unread emails a person has"
    one = "{{.P1}} has {{.PluralCount}} unread email."
    other = "{{.P1}} has {{.PluralCount}} unread emails."
    

    So the first argument is always translated to PluralCount and the rest is numbered, like P1, P2 and so on, and then I can use it like this:

    {{T "PersonUnreadEmails" .NumberOfUnreadEmail .PersonName}}
    

    This works perfectly but is this also how you would do it? Is there a better way?

    opened by zengabor 9
  • Preferences don't cascade on translation

    Preferences don't cascade on translation

    Hey, @nicksnyder! Ran into an issue today you might find interesting. Here's my situation.

    I have three files, all for English: en.all.json, en-US.all.json, and en-GB.all.json. The first holds all English translations that are common between all variations of English we support. The second and third files hold translations specific to their respective locales.

    A user requests something from us, and sends us the locale en-US. We make a TranslationFunc with preferences en-US, en. Now here's where we run into a problem: if I ask for a translation that is not in en-US.all.json, it returns the translationID instead of looking inside en.all.json for a relevant translation.

    This is due to the way bundle.TfuncAndLanguage handles language preferences. When I ask for en-US and the language has any translations, it limits the search to this map, ignoring the remaining language preferences.

    I'd like to support the preference fallback at the translation level, instead of the language level. In pseudocode, this is:

    translation_id = "my_string"
    preferences = %[en-US en]
    
    # Upon request for a translation, iterate through each & return
    # if any of the preferences contain the translation.
    preferences.each do |pref|
      if translations[pref] && translations[pref][translation_id]
        # A translation was found.
        return translations[pref][translation_id]
      end
    end
    # If no langs have matching translations, return the translation ID.
    return translation_id
    

    What do you think?

    enhancement 
    opened by parkr 9
  • example ?

    example ?

    Hello

    I am looking for a way to build translation into a web-based project. I came across this and it really looks impressive. However, I have no idea where to start; I am puzzled how the whole framework is to be integrated in a project. It would be great if there was a minimum sample project somewhere, showing how this is done ?

    opened by Gys 9
  • Export SupportedLanguage

    Export SupportedLanguage

    I'd like to know which language go-i18n decided to use among the candidates caller provided to Tfunc, for example save the language into database for pushing localized content afterwards.

    opened by mash 9
  • How to generate translations for the toml files

    How to generate translations for the toml files

    Hi, I was looking at the examples. One thing I am missing is - How to exactly translate all the messages in the translate.*.toml files? For po files there are some tools like poedit. Similary do they exist for json or toml files?

    opened by sujatapatnaik52 3
  • build a

    build a "example" label

    currently label

    https://github.com/nicksnyder/go-i18n/labels

    Many people ask a question that is for getting an example, so I think to build a label for it.

    opened by CarsonSlovoka 0
  • Fix artificial language matching

    Fix artificial language matching

    This works around what seems to be an upstream by implementing a simplified tag matcher for artificual languages.

    Fixes #252

    I have run the relevant Hugo benchmarks (which I'm more familiar with) and, not surprisingly, this is faster in the "artificial case", other than that it should be about the same.

    name                                              old time/op    new time/op    delta
    I18nTranslate/all-present-16                         930ns ± 1%     926ns ± 0%     ~     (p=0.343 n=4+4)
    I18nTranslate/present-in-default-16                 1.63µs ± 1%    1.62µs ± 1%     ~     (p=0.143 n=4+4)
    I18nTranslate/present-in-current-16                  933ns ± 0%     922ns ± 1%   -1.14%  (p=0.029 n=4+4)
    I18nTranslate/missing-16                            1.50µs ± 0%    1.49µs ± 0%     ~     (p=0.114 n=4+4)
    I18nTranslate/file-missing-16                       3.05µs ± 0%    3.07µs ± 0%   +0.67%  (p=0.029 n=4+4)
    I18nTranslate/context-provided-16                   2.36µs ± 1%    2.33µs ± 0%   -1.24%  (p=0.029 n=4+4)
    I18nTranslate/readingTime-one-16                     671ns ± 1%     665ns ± 1%     ~     (p=0.314 n=4+4)
    I18nTranslate/readingTime-many-dot-16               1.45µs ± 1%    1.41µs ± 0%   -3.21%  (p=0.029 n=4+4)
    I18nTranslate/readingTime-many-16                   2.76µs ± 1%    2.86µs ± 0%   +3.85%  (p=0.029 n=4+4)
    I18nTranslate/readingTime-map-one-16                 723ns ± 1%     725ns ± 0%     ~     (p=0.486 n=4+4)
    I18nTranslate/readingTime-string-one-16              712ns ± 2%     702ns ± 1%     ~     (p=0.114 n=4+4)
    I18nTranslate/readingTime-map-many-16               1.74µs ± 0%    1.69µs ± 2%   -2.83%  (p=0.029 n=4+4)
    I18nTranslate/argument-float-16                     1.44µs ± 0%    1.39µs ± 1%   -3.54%  (p=0.029 n=4+4)
    I18nTranslate/same-id-and-translation-16             916ns ± 2%     915ns ± 1%     ~     (p=0.886 n=4+4)
    I18nTranslate/same-id-and-translation-default-16    1.61µs ± 0%    1.61µs ± 0%     ~     (p=0.114 n=4+4)
    I18nTranslate/unknown-language-code-16              3.85µs ± 0%    3.18µs ± 1%  -17.31%  (p=0.029 n=4+4)
    I18nTranslate/known-language-missing-plural-16      1.57µs ± 2%    0.89µs ± 3%  -43.12%  (p=0.029 n=4+4)
    I18nTranslate/dotted-bare-key-16                     903ns ± 1%     909ns ± 1%     ~     (p=0.343 n=4+4)
    I18nTranslate/lang-with-hyphen-16                    826ns ± 1%     827ns ± 0%     ~     (p=1.000 n=4+4)
    
    name                                              old alloc/op   new alloc/op   delta
    I18nTranslate/all-present-16                          384B ± 0%      384B ± 0%     ~     (all equal)
    I18nTranslate/present-in-default-16                   496B ± 0%      496B ± 0%     ~     (all equal)
    I18nTranslate/present-in-current-16                   384B ± 0%      384B ± 0%     ~     (all equal)
    I18nTranslate/missing-16                              496B ± 0%      496B ± 0%     ~     (all equal)
    I18nTranslate/file-missing-16                         672B ± 0%      672B ± 0%     ~     (all equal)
    I18nTranslate/context-provided-16                     360B ± 0%      360B ± 0%     ~     (all equal)
    I18nTranslate/readingTime-one-16                     48.0B ± 0%     48.0B ± 0%     ~     (all equal)
    I18nTranslate/readingTime-many-dot-16                 232B ± 0%      232B ± 0%     ~     (all equal)
    I18nTranslate/readingTime-many-16                     416B ± 0%      416B ± 0%     ~     (all equal)
    I18nTranslate/readingTime-map-one-16                 48.0B ± 0%     48.0B ± 0%     ~     (all equal)
    I18nTranslate/readingTime-string-one-16              48.0B ± 0%     48.0B ± 0%     ~     (all equal)
    I18nTranslate/readingTime-map-many-16                 264B ± 0%      264B ± 0%     ~     (all equal)
    I18nTranslate/argument-float-16                       224B ± 0%      224B ± 0%     ~     (all equal)
    I18nTranslate/same-id-and-translation-16              384B ± 0%      384B ± 0%     ~     (all equal)
    I18nTranslate/same-id-and-translation-default-16      496B ± 0%      496B ± 0%     ~     (all equal)
    I18nTranslate/unknown-language-code-16                512B ± 0%      440B ± 0%  -14.06%  (p=0.029 n=4+4)
    I18nTranslate/known-language-missing-plural-16        136B ± 0%       72B ± 0%  -47.06%  (p=0.029 n=4+4)
    I18nTranslate/dotted-bare-key-16                      384B ± 0%      384B ± 0%     ~     (all equal)
    I18nTranslate/lang-with-hyphen-16                    48.0B ± 0%     48.0B ± 0%     ~     (all equal)
    
    name                                              old allocs/op  new allocs/op  delta
    I18nTranslate/all-present-16                          3.00 ± 0%      3.00 ± 0%     ~     (all equal)
    I18nTranslate/present-in-default-16                   8.00 ± 0%      8.00 ± 0%     ~     (all equal)
    I18nTranslate/present-in-current-16                   3.00 ± 0%      3.00 ± 0%     ~     (all equal)
    I18nTranslate/missing-16                              8.00 ± 0%      8.00 ± 0%     ~     (all equal)
    I18nTranslate/file-missing-16                         15.0 ± 0%      15.0 ± 0%     ~     (all equal)
    I18nTranslate/context-provided-16                     9.00 ± 0%      9.00 ± 0%     ~     (all equal)
    I18nTranslate/readingTime-one-16                      1.00 ± 0%      1.00 ± 0%     ~     (all equal)
    I18nTranslate/readingTime-many-dot-16                 5.00 ± 0%      5.00 ± 0%     ~     (all equal)
    I18nTranslate/readingTime-many-16                     12.0 ± 0%      12.0 ± 0%     ~     (all equal)
    I18nTranslate/readingTime-map-one-16                  1.00 ± 0%      1.00 ± 0%     ~     (all equal)
    I18nTranslate/readingTime-string-one-16               1.00 ± 0%      1.00 ± 0%     ~     (all equal)
    I18nTranslate/readingTime-map-many-16                 7.00 ± 0%      7.00 ± 0%     ~     (all equal)
    I18nTranslate/argument-float-16                       5.00 ± 0%      5.00 ± 0%     ~     (all equal)
    I18nTranslate/same-id-and-translation-16              3.00 ± 0%      3.00 ± 0%     ~     (all equal)
    I18nTranslate/same-id-and-translation-default-16      8.00 ± 0%      8.00 ± 0%     ~     (all equal)
    I18nTranslate/unknown-language-code-16                17.0 ± 0%      13.0 ± 0%  -23.53%  (p=0.029 n=4+4)
    I18nTranslate/known-language-missing-plural-16        6.00 ± 0%      2.00 ± 0%  -66.67%  (p=0.029 n=4+4)
    I18nTranslate/dotted-bare-key-16                      3.00 ± 0%      3.00 ± 0%     ~     (all equal)
    I18nTranslate/lang-with-hyphen-16                     1.00 ± 0%      1.00 ± 0%     ~     (all equal)
    
    
    opened by bep 4
  • Fails to resolve with multiple artificial languages

    Fails to resolve with multiple artificial languages

    github.com/nicksnyder/go-i18n
    

    The following test fails:

    func TestPseudoLanguages(t *testing.T) {
    	bundle := NewBundle(language.English)
    	bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
    	expected := "a3"
    	bundle.MustParseMessageFileBytes([]byte(`
    hello = "a1"
    `), "art-x-a1.toml")
    	bundle.MustParseMessageFileBytes([]byte(`
    hello = "a2"
    `), "art-x-a2.toml")
    	bundle.MustParseMessageFileBytes([]byte(`
    hello = "a3"
    `), "art-x-a3.toml")
    
    	{
    		localized, err := NewLocalizer(bundle, "art-x-a3").Localize(&LocalizeConfig{MessageID: "hello"})
    		if err != nil {
    			t.Fatal(err)
    		}
    		if localized != expected {
    			t.Fatalf("expected %q\ngot %q", expected, localized)
    		}
    	}
    }
    

    I tried to debug this -- looks like it's some odd bug in the upstream x/text Matcher.

    https://github.com/gohugoio/hugo/issues/7838

    opened by bep 1
  • Adding gettext

    Adding gettext

    We've really enjoyed this package. One thing that would help us tremendously is if we could share translation files between platforms. What would need to happen to make a gettext unmarhaller that could seamlessly read in a Django-style collection of po/pm translations? As is, we've had to write tools to turn the latter into the former. That works, more or less, but having a single, shared source of translations would be much more desirable.

    enhancement 
    opened by Waitak 1
Releases(v2.2.0)
Owner
Nick Snyder
Nick Snyder
📖 Tutorial: An easy way to translate your Golang application

?? Tutorial: An easy way to translate your Golang application ?? The full article is published on April 13, 2021, on Dev.to: https://dev.to/koddr/an-e

Vic Shóstak 6 Feb 9, 2022
Complete Translation - translate a document to another language

Complete Translation This project is to translate a document to another language. The initial target is English to Korean. Consider this project is no

코딩냄비 4 Feb 25, 2022
[UNMANTEINED] Extract values from strings and fill your structs with nlp.

nlp nlp is a general purpose any-lang Natural Language Processor that parses the data inside a text and returns a filled model Supported types int in

Juan Alvarez 379 Jul 28, 2022
Package i18n provides internationalization and localization for your Go applications.

i18n Package i18n provides internationalization and localization for your Go applications. Installation The minimum requirement of Go is 1.16. go get

null 53 Aug 8, 2022
i18n-pseudo - Pseudolocalization is an incredibly useful tool for localizing your apps.

i18n-pseudo Pseudolocalization is an incredibly useful tool for localizing your apps. This module makes it easy to apply pseudo to any given string. I

Fetch Rewards 2 Mar 21, 2022
Translate your Go program into multiple languages with similar fmt.Sprintf format syntax.

Loafer-i18n Loafer-i18n is a Go package and a command that helps you translate Go programs into multiple languages. Supports pluralized strings with =

EasyGram 0 Dec 22, 2021
go-i18n is a Go package and a command that helps you translate Go programs into multiple languages.

go-i18n is a Go package and a command that helps you translate Go programs into multiple languages.

Nick Snyder 2.1k Sep 20, 2022
Tool that can parse Go files into an abstract syntax tree and translate it to several programming languages.

GoDMT GoDMT, the one and only Go Data Model Translator. The goal of this project is to provide a tool that can parse Go files that include var, const,

Josep Jesus Bigorra Algaba 42 Jan 29, 2022
Show Languages In Code. A fast and lightweight CLI to generate stats on the languages inside your project

slic Show Languages In Code. Usage Run it with an -h flag to list all commands. -d flag can be used to specify the directory of search -i flag can be

Saurav Pal 3 Dec 25, 2021
Translate Prometheus Alerts into Kubernetes pod readiness

prometheus-alert-readiness Translates firing Prometheus alerts into a Kubernetes readiness path. Why? By running this container in a singleton deploym

Coralogix 19 Mar 7, 2021
Cli tool to translate text from any language into german

GERMAN A cli tool for converting text into German. Build Locally $> go build $> go install Dependencies To execute successfully, a free tier DEEPL API

Kieran O'Sullivan 3 Jan 24, 2022
📖 Tutorial: An easy way to translate your Golang application

?? Tutorial: An easy way to translate your Golang application ?? The full article is published on April 13, 2021, on Dev.to: https://dev.to/koddr/an-e

Vic Shóstak 6 Feb 9, 2022
URL-friendly slugify with multiple languages support.

slug Package slug generate slug from unicode string, URL-friendly slugify with multiple languages support. Documentation online Example package main

GoSimple 862 Sep 28, 2022
Stalin sort in multiple languages!

stalin-sort Stalin sort in multiple languages, contributions are welcome! Motivation This repo is motivated by this tweet, this tweet contains a refer

CaKrome 0 Jan 14, 2022
libraries for various programming languages that make it easy to generate per-process trace files that can be loaded into chrome://tracing

chrometracing: chrome://tracing trace_event files The chrometracing directory contains libraries for various programming languages that make it easy t

Google 22 Jul 8, 2022
Split multiple Kubernetes files into smaller files with ease. Split multi-YAML files into individual files.

Split multiple Kubernetes files into smaller files with ease. Split multi-YAML files into individual files.

Patrick D'appollonio 185 Sep 26, 2022
Split multiple Kubernetes files into smaller files with ease. Split multi-YAML files into individual files.

kubectl-slice: split Kubernetes YAMLs into files kubectl-slice is a neat tool that allows you to split a single multi-YAML Kubernetes manifest into mu

Patrick D'appollonio 185 Sep 26, 2022
koanfenv provides koanf callbacks that translate environment variables to koanf keys.

koanfenv koanfenv provides callbacks which convert environment variables to koanf keys. These callbacks are used for env.Provider . Usage config := st

Wade Zhang 0 Dec 12, 2021
Translate numbers from numerals to words or roman numerals

Word-Number This package provides a methods for converting numbers to/from various 'word' representations. Currently, that means cardinal and ordinal

Julian Peterson 1 May 23, 2022
Complete Translation - translate a document to another language

Complete Translation This project is to translate a document to another language. The initial target is English to Korean. Consider this project is no

코딩냄비 4 Feb 25, 2022