Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flatter data file structure #65

Merged
merged 8 commits into from
Mar 19, 2017
Merged

Conversation

n10v
Copy link
Contributor

@n10v n10v commented Feb 20, 2017

Fixes #62
Updates #61

TODO:

  • Add docs

Please leave your opinions and comments.

/cc @bep @moorereason @nicksnyder

Copy link
Owner

@nicksnyder nicksnyder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Sorry for the delay in reviewing.

Just a few comments.

var standardFormat []map[string]interface{}
err := unmarshal(filename, buf, &standardFormat)
if err == nil {
translations, err := parseStandardFormat(standardFormat)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can just be

return parseStandardFormat(standardFormat)

if err := unmarshal(filename, buf, &flatterFormat); err != nil {
return nil, err
}
translations, err := parseFlatterFormat(flatterFormat)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return parseFlatterFormat(flatterFormat)

}
translations = append(translations, t)
}
return translations, nil
}

func parseFlatterFormat(data map[string]map[string]interface{}) ([]translation.Translation, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's just call this parseFlatFormat (and update files to be .flat.json)

if err := unmarshalFunc(buf, &translationsData); err != nil {
return nil, fmt.Errorf("failed to load %s because %s", filename, err)
}
return unmarshalFunc(buf, out)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add back the error message? It is nice to know which file the error happened in.

if err := unmarshalFunc(buf, &translationsData); err != nil {
	return fmt.Errorf("failed to load %s because %s", filename, err)
}


T, err := b.Tfunc("en-US")
if err != nil {
panic(err)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no reason to panic in a test, just fail the test

initTestCases()
}

func initTestCases() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to create a separate method, just do this in init

}

func initTestCases() {
b := bundle.New()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't appear necessary to create a bundle during initialization

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used for creating a Tfunc and Tfunc is used for checking "d_days" in init (person_unread_email_count_timeframe).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. This is weird because you are reading the JSON file during init but testing other formats in the actual tests.

I would actually not try to do anything in init. Since it is just a test, it is fine to re-init the small array of test cases for each test.

}

for _, tc := range testCases {
t.Run(tc.want, func(t *testing.T) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using Run means tests don't compile on Go < 1.7. Can we do without?

@@ -0,0 +1,34 @@
{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is a copy of en-us.all.json, can we call this en-us.all.flat.json?

@@ -0,0 +1,25 @@
program_greeting:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

en-us.all.flat.yaml

@n10v n10v force-pushed the flatter branch 2 times, most recently from 31b1e16 to 72b2d34 Compare March 6, 2017 12:40
@@ -118,6 +118,49 @@ Here is an example of the default file format that go-i18n supports:

To use a different file format, write a parser for the format and add the parsed translations using [AddTranslation](https://godoc.org/github.com/nicksnyder/go-i18n/i18n#AddTranslation).

Flat Format
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to update the goi18n merge command to have an option to output the flat format before we advertise this on the main readme. Up to you whether you want to just remove the README update from this PR or update the merge command.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you offer to implement this? I propose to add a new boolean flag called -flat to output data in flat format

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

func initTestCases() {
b := bundle.New()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. This is weird because you are reading the JSON file during init but testing other formats in the actual tests.

I would actually not try to do anything in init. Since it is just a test, it is fine to re-init the small array of test cases for each test.

@moorereason
Copy link

I'm not familiar with the internals of the JSON and YAML packages. Do they return early if the data structure doesn't match the target interface? Can the caller avoid a double-unmarshal when using the flat format?

@n10v
Copy link
Contributor Author

n10v commented Mar 8, 2017

@moorereason reading the docs of encoding/json and gopkg.in/yaml.v2 I didn't find any method to check the data structure, but in json lib there is a variety of errors (SyntaxError, UnmarshalFieldError, UnmarshalTypeError and etc.), so we can know, if it's flat format or a user just mistook in json syntax (forgot semicolon, colon, curly bracket and etc.)

@bep
Copy link
Contributor

bep commented Mar 8, 2017

This change was motivated by another format, TOML, so having special casings for JSON is sub-optimal.

I think the current implementation is great as is.

If there is a simple way to detect the format ala:

isFlat := bytes.Contains(buf[:100], []byte("someflatidentifier")

Then that would be great, but I would not add it if it adds complexity.

@n10v
Copy link
Contributor Author

n10v commented Mar 8, 2017

The standard format could be detected this way:

isStandard := bytes.Contains(data[:100], []byte("id")) && (bytes.Contains(data[:100], []byte("translation")) || bytes.Contains(data[:100], []byte("translations")))

The problem could be if id will be more than ~95 bytes length, so it's not the right way. But actually this method only reduces complexity.

@n10v
Copy link
Contributor Author

n10v commented Mar 9, 2017

@bep TOML doesn't support standard format layout. That's why we should add flat format
If user passes TOML translation file, it will be automatically parsed as flat format. So we don't need to detect the format for TOML. At least I will implement like that

@n10v
Copy link
Contributor Author

n10v commented Mar 9, 2017

I think, the simplest and right way to detect the format is built on the first byte.
If it's JSON and first byte of data is [ or if it's YAML and first byte is -, then it's standard format, else it's flat format.
What do you think about it?

@nicksnyder
Copy link
Owner

I know TOML isn't part of this PR (and it shouldn't be), but what would the equivalent TOML file look like?

I think, the simplest and right way to detect the format is built on the first byte.
If it's JSON and first byte of data is [ or if it's YAML and first byte is -, then it's standard format, else it's flat format.

This sounds right to me.

@n10v
Copy link
Contributor Author

n10v commented Mar 9, 2017

@nicksnyder

@bep TOML doesn't support standard format layout. That's why we should add flat format
If user passes TOML translation file, it will be automatically parsed as flat format. So we don't need to detect the format for TOML. At least I will implement like that

@n10v n10v force-pushed the flatter branch 3 times, most recently from 9720d87 to 37787b5 Compare March 9, 2017 16:16
@n10v
Copy link
Contributor Author

n10v commented Mar 15, 2017

So I updated merge command and made tests for it. What do you think about it?

Copy link
Owner

@nicksnyder nicksnyder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BoGeM Thanks for updating! Just a few more comments and then I will merge it.

Can you also reply with what this will look like in TOML?

case ".json":
unmarshalFunc = json.Unmarshal
err = json.Unmarshal(buf, out)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just return json.Unmarshal(buf, out)

case ".yaml":
unmarshalFunc = yaml.Unmarshal
err = yaml.Unmarshal(buf, out)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

sort.Sort(translation.SortableByID(translations))
buf, err := marshal(marshalInterface(translations))

var iTranslations interface{}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of this variable name. I would just avoid it though

var marshal func ([]translation.Translation) interface{}
if mc.flat {
    marshal = marshalFlatInterface
} else {
    marshal = marshalInterface
}

buf, err := mc.marshal(marshal(translations))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mc.marshal(marshal(translations)) - it's also very strange.
I offer to name it as conv2I or just convert.

@n10v
Copy link
Contributor Author

n10v commented Mar 19, 2017

@nicksnyder I've already told you: TOML doesn't support standard format layout. That's why we should add flat format. If user passes TOML translation file, it will be automatically parsed as flat format. So we don't need to detect the format for TOML. At least I will implement like that

@nicksnyder
Copy link
Owner

nicksnyder commented Mar 19, 2017 via email

@n10v
Copy link
Contributor Author

n10v commented Mar 19, 2017

@nicksnyder

func isStandardFormat(ext string, buf []byte) bool {
  if ext == ".toml" {
    return false
  }

  firstRune := rune(buf[0])
  if (ext == ".json" && firstRune == '[') || (ext == ".yaml" && firstRune == '-') {
    return true
  }
  return false
}

@nicksnyder
Copy link
Owner

Sorry if I wasn't clear. I want to know what en-us.flat.toml or perhaps just en-us.toml will look like.

@nicksnyder nicksnyder merged commit be79775 into nicksnyder:master Mar 19, 2017
@n10v
Copy link
Contributor Author

n10v commented Mar 20, 2017

Oh sorry. I thought, you asked me about detection of TOML 😅
en-us.flat.toml will look like:

[program_greeting]
other = "Hello world"

[person_greeting]
other = "Hello {{.Person}}"

[my_height_in_meters]
one = "I am {{.Count}} meter tall."
other = "I am {{.Count}} meters tall."

[your_unread_email_count]
one = "You have {{.Count}} unread email."
other = "You have {{.Count}} unread emails."

[person_unread_email_count]
one = "{{.Person}} has {{.Count}} unread email."
other = "{{.Person}} has {{.Count}} unread emails."

[person_unread_email_count_timeframe]
one = "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}."
other = "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."

[d_days]
one = "{{.Count}} day."
other = "{{.Count}} days."

@n10v n10v deleted the flatter branch March 20, 2017 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants