{"id":21045139,"url":"https://github.com/fullpipe/icu-mf","last_synced_at":"2025-09-06T01:34:14.696Z","repository":{"id":255677002,"uuid":"852888249","full_name":"fullpipe/icu-mf","owner":"fullpipe","description":"Translate using the ICU MessageFormat","archived":false,"fork":false,"pushed_at":"2025-04-03T15:22:55.000Z","size":86,"stargazers_count":13,"open_issues_count":3,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-15T17:48:18.832Z","etag":null,"topics":["go","golang","i18n","icu","l10n","localization","pluralize","translation"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fullpipe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-09-05T15:46:23.000Z","updated_at":"2025-04-03T15:19:34.000Z","dependencies_parsed_at":"2025-02-10T00:01:04.685Z","dependency_job_id":"e0eb09f4-8b4f-400e-8b27-d6e406e768d4","html_url":"https://github.com/fullpipe/icu-mf","commit_stats":null,"previous_names":["fullpipe/icu-mf"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/fullpipe/icu-mf","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fullpipe%2Ficu-mf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fullpipe%2Ficu-mf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fullpipe%2Ficu-mf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fullpipe%2Ficu-mf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fullpipe","download_url":"https://codeload.github.com/fullpipe/icu-mf/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fullpipe%2Ficu-mf/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273846976,"owners_count":25178631,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-05T02:00:09.113Z","response_time":402,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","i18n","icu","l10n","localization","pluralize","translation"],"created_at":"2024-11-19T14:20:10.326Z","updated_at":"2025-09-06T01:34:14.685Z","avatar_url":"https://github.com/fullpipe.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ICU MessageFormat for Golang\n\n[![test](https://github.com/fullpipe/icu-mf/actions/workflows/test.yml/badge.svg)](https://github.com/fullpipe/icu-mf/actions/workflows/test.yml)\n[![codecov](https://codecov.io/github/fullpipe/icu-mf/graph/badge.svg?token=W6C02M3BFQ)](https://codecov.io/github/fullpipe/icu-mf)\n[![lint](https://github.com/fullpipe/icu-mf/actions/workflows/lint.yml/badge.svg)](https://github.com/fullpipe/icu-mf/actions/workflows/lint.yml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/fullpipe/icu-mf.svg)](https://pkg.go.dev/github.com/fullpipe/icu-mf)\n\nMessages in your application are never static. They have variables, pluralization, and formatting.\nTo translate them easily, use [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/).\n\n## Why?\n\nThere is a great package for translations called [nicksnyder/go-i18n](https://github.com/nicksnyder/go-i18n).\nHowever, once I had a lot of translations in chatbots, it started to feel cumbersome.\n\nSo, I tried to make translations simpler. Now, instead:\n\n```go\nlocalizer.Localize(\u0026i18n.LocalizeConfig{\n    DefaultMessage: \u0026i18n.Message{\n        ID: \"PersonCats\",\n        One: \"{{.Name}} has {{.Count}} cat.\",\n        Other: \"{{.Name}} has {{.Count}} cats.\",\n    },\n    TemplateData: map[string]interface{}{\n        \"Name\": \"Nick\",\n        \"Count\": 2,\n    },\n    PluralCount: 2,\n}) // Nick has 2 cats.\n```\n\nI got:\n\n```go\ntr.Trans(\"person.cats\", mf.Arg(\"name\", \"Nick\"), mf.Arg(\"cats_num\", 2))\n// Nick has 2 cats.\n```\n\n## Usage\n\nImport package\n\n```go\nimport \"github.com/fullpipe/icu-mf/mf\"\n```\n\nLocate messages with go:embed\n\n```go\n//go:embed var/messages.*.yaml\nvar messagesDir embed.FS\n\n// or you could load messages dynamically\nmessagesDir := os.DirFS(\"var\")\n```\n\nCreate translations bundle\n\n```go\nbundle, err := mf.NewBundle(\n    // If not possible to find a message for the specific language, fallback to English (EN)\n    mf.WithDefaultLangFallback(language.English),\n\n    // We could fine-tune fallbacks for some languages\n    mf.WithLangFallback(language.BritishEnglish, language.English),\n    mf.WithLangFallback(language.Portuguese, language.Spanish),\n\n    // Load all yaml files in directory as messages\n    mf.WithYamlProvider(messagesDir),\n\n    // or you could use your own custom message provider\n    // mf.WithProvider(sqlMessageProvider),\n\n    // We assume that the translated messages are mostly correct.\n    // However, if any errors occur during translation,\n    // they will be directed to the error handler.\n    mf.WithErrorHandler(func(err error, id string, ctx map[string]any) {\n        slog.Error(err.Error(), slog.String(\"id\", id), slog.Any(\"ctx\", ctx))\n\n        // or\n        //panic(err)\n    }),\n)\n\nif err != nil {\n    log.Fatal(err)\n}\n```\n\nTranslate messages by their ID\n\n```go\ntr := bundle.Translator(\"en\")\n\ntr.Trans(\"invitation.status\",\n    mf.Arg(\"gender_of_host\", \"female\"),\n    mf.Arg(\"num_guests\", 5),\n    mf.Arg(\"guest\", \"Sionia\"),\n    mf.Arg(\"host\", \"Rina\"),\n) // Rina invites Sionia and 4 other people to her party.\n\n\ntrEs := bundle.Translator(\"es\")\n\ntrEs.Trans(\"say_hello\", mf.Arg(\"name\", \"Aníbal\"))\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eFull example\u003c/summary\u003e\n\n```go\npackage main\n\nimport (\n\t\"embed\"\n\t\"log\"\n\t\"log/slog\"\n\n\t\"github.com/fullpipe/icu-mf/mf\"\n\t\"golang.org/x/text/language\"\n)\n\n//go:embed var/messages.*.yaml\nvar messagesDir embed.FS\n\nfunc main() {\n\tbundle, err := mf.NewBundle(\n\t\tmf.WithDefaultLangFallback(language.English),\n\n\t\tmf.WithLangFallback(language.BritishEnglish, language.English),\n\t\tmf.WithLangFallback(language.Portuguese, language.Spanish),\n\n\t\tmf.WithYamlProvider(messagesDir),\n\n\t\tmf.WithErrorHandler(func(err error, id string, ctx map[string]any) {\n\t\t\tslog.Error(err.Error(), slog.String(\"id\", id), slog.Any(\"ctx\", ctx))\n\t\t}),\n\t)\n\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\ttr := bundle.Translator(\"es\")\n\n\tslog.Info(tr.Trans(\"say_hello\", mf.Arg(\"name\", \"Bob\")))\n}\n```\n\n\u003c/details\u003e\n\n### YAML\n\nYAML allows you to organize your translations in a tree-like structure.\n\n```yaml\nuser:\n  profile:\n    name: My name is {name}\n    age: I'm {age, plural, one {# year} other {# years}} old\n  account_form:\n    username_field: 'Enter your username:'\n    error: \u003e-\n      {name, select\n          required {specify {field}}\n          min {{field} requires at least 10 chars}\n          other {some unknown error with {field}}\n      }\n\npayments: ...\n\nserver:\n  http:\n    404: Page not found\n    503: Oops!\n```\n\nAnd you get messages by their \"path\"\n\n```go\ntr.Trans(\"user.profile.age\", mf.Arg(\"age\", 42))\ntr.Trans(\n    \"user.account_form.error\",\n    mf.Arg(\"name\", \"min\"), mf.Arg(\"field\", \"description\"),\n)\n```\n\n### Escaping\n\nSometimes you need to print `{`, `'`, or `#`. You could escape them with `'` char.\n\n```yaml\n# translations/messages.en.yaml\nescape: \"'{foo} is ''{foo}''\"\n```\n\n```go\ntr.Trans(\"escape\", mf.Arg(\"foo\", \"bar\"))\n// {foo} is 'bar'\n```\n\n## MessageFormat overview\n\n### Placeholders\n\nMessageFormat allows to use placeholders in your messages.\n\n```yaml\n# translations/messages.en.yaml\n\nsay_hello: 'Hello, {name}!'\n```\n\nEverything in `{...}` will be processed as an argument and will be replaced by the provided context arguments.\n\n```go\ntr.Trans(\"say_hello\", mf.Arg(\"name\", \"Bob\"))\n// Hello, Bob!\n```\n\n### Simple select\n\n```yaml\n# translations/messages.en.yaml\n\n# the 'other' key is required, and is selected if no other case matches\ninvitation:\n  title: \u003e-\n    {organizer_gender, select,\n        female   {{organizer_name} has invited you to her party!}\n        male     {{organizer_name} has invited you to his party!}\n        multiple {{organizer_name} have invited you to their party!}\n        other    {{organizer_name} has invited you to their party!}\n    }\n  body: ...\n```\n\n```go\ntr.Trans(\n    \"invitation.title\",\n    mf.Arg(\"organizer_name\", \"Ryan\"),\n    mf.Arg(\"organizer_gender\", \"male\"),\n) // Ryan has invited you to his party!\n\ntr.Trans(\n    \"invitation.title\",\n    mf.Arg(\"organizer_name\", \"John \u0026 Jane\"),\n    mf.Arg(\"organizer_gender\", \"multiple\"),\n) // John \u0026 Jane have invited you to their party!\n\ntr.Trans(\n    \"invitation.title\",\n    mf.Arg(\"organizer_name\", \"ACME Company\"),\n    mf.Arg(\"organizer_gender\", \"not_applicable\"),\n) // ACME Company has invited you to their party!\n```\n\nAs you can see, the `{...}` syntax behaves differently here:\n\n1. The first `{organizer_gender, select, ...}` block starts \"code\" mode, meaning `organizer_gender` is processed as a variable.\n2. The inner `{... has invited you to her party!}` block switches to \"literal\" mode, meaning the text inside is processed as sub-message.\n3. Inside this block, `{organizer_name}` starts \"code\" mode again, allowing `organizer_name` to be processed as a variable.\n\n### Pluralization\n\nThere is another function, `plural`, similar to `select`. It allows you to handle pluralization in your messages (e.g., `There are 3 apples` vs. `There is one apple`).\n\n```yaml\n# translations/messages.en.yaml\nnum_of_apples: \u003e-\n  {apples, plural,\n      =0    {I don't have an apple}\n      one   {I have one apple}\n      other {I have # apples!}\n  }\n```\n\nPluralization rules are actually quite complex and differ for each language.\nFor instance, Russian uses different plural forms for numbers ending with 1;\nnumbers ending with 2, 3, or 4; numbers ending with 5, 6, 7, 8, or 9;\nand even some exceptions to this!\n\nTo properly translate plural forms, the possible cases in the `plural` function\nare also different for each language. For instance, Russian has `one`, `few`, `many`,\nand `other`, while English has only `one` and `other`.\nThe full list of possible cases can be found\nin Unicode's [Language Plural Rules](https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html) document.\n\nBy prefixing with `=`, you can match exact values (like 0 in the above example).\n\n```yaml\n# translations/messages.ru.yaml\n\nnum_of_apples: \u003e-\n  {apples, plural,\n      =0    {У меня нет яблок}\n      =1    {У меня одно яблоко}\n      one   {У меня # яблоко}\n      few   {У меня # яблока}\n      many  {У меня # яблок}\n      other {У меня # яблок}\n  }\n```\n\nThe usage of this string is the same as with `select`:\n\n```go\n// for EN\n\ntr.Trans(\"num_of_apples\", mf.Arg(\"apples\", 5))\n// I have 5 apples!\n\n// for RU\ntrRU.Trans(\"num_of_apples\", mf.Arg(\"apples\", 3))\n// У меня 3 яблока\n\n```\n\nYou can use the `#` placeholder to display the pluralized number.\n\n#### Offset\n\nYou can also set an `offset` variable to determine whether the pluralization should be adjusted. For example, in sentences like `You and # other people` / `You and # other person`.\n\n```yaml\n# translations/messages.en.yaml\nparty_status: \u003e-\n  {num_guests, plural, offset:1\n      =0    {{host} does not give a party.}\n      =1    {{host} invites {guest} to her party.}\n      =2    {{host} invites {guest} and one other person to her party.}\n      other {{host} invites {guest} and # other people to her party.}\n  }\n```\n\n```go\ntr.Trans(\n    \"party_status\",\n    mf.Arg(\"num_guests\", 1),\n    mf.Arg(\"host\", \"Rogna\"),\n    mf.Arg(\"guest\", \"Azog\"),\n) // Rogna invites Azog to her party.\n\ntr.Trans(\n    \"party_status\",\n    mf.Arg(\"num_guests\", 5),\n    mf.Arg(\"host\", \"Rogna\"),\n    mf.Arg(\"guest\", \"Azog\"),\n) // Rogna invites Azog and 4 other people to her party.\n```\n\nFirst, we compare `num_guests` with the strict cases `=0`, `=1`, and `=2`.\nIf nothing matches, we subtract the `offset`, `num_guests = num_guests - offset`,\nand then determine the plural case based on the result.\n\n#### Nesting\n\nYou could make pretty complex nested messages if needed.\n\n```yaml\n# translations/messages.en.yaml\n\ninvitation_status: \u003e-\n  {gender_of_host, select,\n      female {{num_guests, plural, offset:1\n          =0    {{host} does not give a party.}\n          =1    {{host} invites {guest} to her party.}\n          =2    {{host} invites {guest} and one other person to her party.}\n          other {{host} invites {guest} and # other people to her party.}\n      }}\n      male {{num_guests, plural, offset:1\n          =0    {{host} does not give a party.}\n          =1    {{host} invites {guest} to his party.}\n          =2    {{host} invites {guest} and one other person to his party.}\n          other {{host} invites {guest} and # other people to his party.}\n      }}\n      other {{num_guests, plural, offset:1\n          =0    {{host} does not give a party.}\n          =1    {{host} invites {guest} to their party.}\n          =2    {{host} invites {guest} and one other person to their party.}\n          other {{host} invites {guest} and # other people to their party.}\n      }}\n  }\n```\n\n#### Inline\n\nCases in `plural`, `select` or `selectordinal` could be inlined\n\n```yaml\n# translations/messages.en.yaml\nnum_of_apples: 'There {apples, plural, =0 {are no} one {is one} other {are # apples}} apples'\n```\n\n### Additional Functions\n\n#### Ordinal\n\nSimilar to `plural`, `selectordinal` allows you to use numbers as ordinal scale:\n\n```yaml\n# translations/messages.en.yaml\nfinish_place: \u003e-\n  You finished {place, selectordinal,\n      one   {#st}\n      two   {#nd}\n      few   {#rd}\n      other {#th}\n  }!\n```\n\n```go\ntr.Trans(\"finish_place\", mf.Arg(\"place\", 1))\n// You finished 1st!\n\ntr.Trans(\"finish_place\", mf.Arg(\"place\", 9))\n// You finished 9th!\n\ntr.Trans(\"finish_place\", mf.Arg(\"place\", 43))\n// You finished 43rd!\n```\n\nThe possible cases for this are also shown in Unicode's [Language Plural Rules](https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html) document.\n\n#### Numbers\n\nThere are some minor functions to work with numbers and dates.\n\n##### Integer\n\n```yaml\n# translations/messages.en.yaml\n\nbig_num: big number {num, number, integer}!\n\n\n# translations/messages.es.yaml\n\nbig_num: gran numero {num, number, integer}!\n```\n\n```go\ntr.Trans(\"big_num\", mf.Arg(\"num\", 123456789))\n// big number 123,456,789!\n\nbundle.Translator(\"es\").Trans(\"big_num\", mf.Arg(\"num\", 123456789))\n// gran numero 123.456.789!\n```\n\n##### Percent\n\n```yaml\n# translations/messages.en.yaml\n\ntest_cover: we got {cover, number, percent} test coverage!\n```\n\n```go\ntr.Trans(\"test_cover\", mf.Arg(\"cover\", 0.42))\n// we got 42% test coverage!\n\ntr.Trans(\"test_cover\", mf.Arg(\"cover\", 1))\n// we got 100% test coverage!\n```\n\n#### Date and Time\n\nThere are `date`, `time`, and `datetime` functions to format `time.Time` arguments.\nAdditionally, there are four different formats: `short`, `medium`, `long`, and `full`.\n\n```yaml\n# translations/messages.en.yaml\n\nvostok:\n  start: Vostok-1 start {start_date, datetime, long}.\n  landing: Vostok-1 landing time {land_time, time, medium}.\napollo:\n  step: First step on the Moon on {step_date, date, long}.\n```\n\n```go\nstart := time.Date(1961, 4, 12, 6, 7, 3, 0, time.UTC)\nland := time.Date(1961, 4, 12, 7, 55, 0, 0, time.UTC)\nstep := time.Date(1969, 7, 21, 2, 56, 0, 0, time.UTC)\n\ntr.Trans(\"vostok.start\", mf.Time(\"start_date\", start))\n// Vostok-1 start April 12, 1961 at 6:07:03 AM UTC.\n\ntr.Trans(\"vostok.landing\", mf.Time(\"land_time\", land))\n// Vostok-1 landing time 7:55:00 AM.\n\ntr.Trans(\"apollo.step\", mf.Time(\"step_date\", step))\n// First step on the Moon on July 21, 1969.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffullpipe%2Ficu-mf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffullpipe%2Ficu-mf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffullpipe%2Ficu-mf/lists"}