You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

815 lines
19 KiB

  1. // Copyright 2016 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package pipeline
  5. import (
  6. "bytes"
  7. "errors"
  8. "fmt"
  9. "go/ast"
  10. "go/constant"
  11. "go/format"
  12. "go/token"
  13. "go/types"
  14. "path/filepath"
  15. "strings"
  16. "unicode"
  17. "unicode/utf8"
  18. fmtparser "golang.org/x/text/internal/format"
  19. "golang.org/x/tools/go/callgraph"
  20. "golang.org/x/tools/go/callgraph/cha"
  21. "golang.org/x/tools/go/loader"
  22. "golang.org/x/tools/go/ssa"
  23. "golang.org/x/tools/go/ssa/ssautil"
  24. )
  25. const debug = false
  26. // TODO:
  27. // - merge information into existing files
  28. // - handle different file formats (PO, XLIFF)
  29. // - handle features (gender, plural)
  30. // - message rewriting
  31. // - `msg:"etc"` tags
  32. // Extract extracts all strings form the package defined in Config.
  33. func Extract(c *Config) (*State, error) {
  34. x, err := newExtracter(c)
  35. if err != nil {
  36. return nil, wrap(err, "")
  37. }
  38. if err := x.seedEndpoints(); err != nil {
  39. return nil, err
  40. }
  41. x.extractMessages()
  42. return &State{
  43. Config: *c,
  44. program: x.iprog,
  45. Extracted: Messages{
  46. Language: c.SourceLanguage,
  47. Messages: x.messages,
  48. },
  49. }, nil
  50. }
  51. type extracter struct {
  52. conf loader.Config
  53. iprog *loader.Program
  54. prog *ssa.Program
  55. callGraph *callgraph.Graph
  56. // Calls and other expressions to collect.
  57. globals map[token.Pos]*constData
  58. funcs map[token.Pos]*callData
  59. messages []Message
  60. }
  61. func newExtracter(c *Config) (x *extracter, err error) {
  62. x = &extracter{
  63. conf: loader.Config{},
  64. globals: map[token.Pos]*constData{},
  65. funcs: map[token.Pos]*callData{},
  66. }
  67. x.iprog, err = loadPackages(&x.conf, c.Packages)
  68. if err != nil {
  69. return nil, wrap(err, "")
  70. }
  71. x.prog = ssautil.CreateProgram(x.iprog, ssa.GlobalDebug|ssa.BareInits)
  72. x.prog.Build()
  73. x.callGraph = cha.CallGraph(x.prog)
  74. return x, nil
  75. }
  76. func (x *extracter) globalData(pos token.Pos) *constData {
  77. cd := x.globals[pos]
  78. if cd == nil {
  79. cd = &constData{}
  80. x.globals[pos] = cd
  81. }
  82. return cd
  83. }
  84. func (x *extracter) seedEndpoints() error {
  85. pkgInfo := x.iprog.Package("golang.org/x/text/message")
  86. if pkgInfo == nil {
  87. return errors.New("pipeline: golang.org/x/text/message is not imported")
  88. }
  89. pkg := x.prog.Package(pkgInfo.Pkg)
  90. typ := types.NewPointer(pkg.Type("Printer").Type())
  91. x.processGlobalVars()
  92. x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Printf"), &callData{
  93. formatPos: 1,
  94. argPos: 2,
  95. isMethod: true,
  96. })
  97. x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Sprintf"), &callData{
  98. formatPos: 1,
  99. argPos: 2,
  100. isMethod: true,
  101. })
  102. x.handleFunc(x.prog.LookupMethod(typ, pkg.Pkg, "Fprintf"), &callData{
  103. formatPos: 2,
  104. argPos: 3,
  105. isMethod: true,
  106. })
  107. return nil
  108. }
  109. // processGlobalVars finds string constants that are assigned to global
  110. // variables.
  111. func (x *extracter) processGlobalVars() {
  112. for _, p := range x.prog.AllPackages() {
  113. m, ok := p.Members["init"]
  114. if !ok {
  115. continue
  116. }
  117. for _, b := range m.(*ssa.Function).Blocks {
  118. for _, i := range b.Instrs {
  119. s, ok := i.(*ssa.Store)
  120. if !ok {
  121. continue
  122. }
  123. a, ok := s.Addr.(*ssa.Global)
  124. if !ok {
  125. continue
  126. }
  127. t := a.Type()
  128. for {
  129. p, ok := t.(*types.Pointer)
  130. if !ok {
  131. break
  132. }
  133. t = p.Elem()
  134. }
  135. if b, ok := t.(*types.Basic); !ok || b.Kind() != types.String {
  136. continue
  137. }
  138. x.visitInit(a, s.Val)
  139. }
  140. }
  141. }
  142. }
  143. type constData struct {
  144. call *callData // to provide a signature for the constants
  145. values []constVal
  146. others []token.Pos // Assigned to other global data.
  147. }
  148. func (d *constData) visit(x *extracter, f func(c constant.Value)) {
  149. for _, v := range d.values {
  150. f(v.value)
  151. }
  152. for _, p := range d.others {
  153. if od, ok := x.globals[p]; ok {
  154. od.visit(x, f)
  155. }
  156. }
  157. }
  158. type constVal struct {
  159. value constant.Value
  160. pos token.Pos
  161. }
  162. type callData struct {
  163. call ssa.CallInstruction
  164. expr *ast.CallExpr
  165. formats []constant.Value
  166. callee *callData
  167. isMethod bool
  168. formatPos int
  169. argPos int // varargs at this position in the call
  170. argTypes []int // arguments extractable from this position
  171. }
  172. func (c *callData) callFormatPos() int {
  173. c = c.callee
  174. if c.isMethod {
  175. return c.formatPos - 1
  176. }
  177. return c.formatPos
  178. }
  179. func (c *callData) callArgsStart() int {
  180. c = c.callee
  181. if c.isMethod {
  182. return c.argPos - 1
  183. }
  184. return c.argPos
  185. }
  186. func (c *callData) Pos() token.Pos { return c.call.Pos() }
  187. func (c *callData) Pkg() *types.Package { return c.call.Parent().Pkg.Pkg }
  188. func (x *extracter) handleFunc(f *ssa.Function, fd *callData) {
  189. for _, e := range x.callGraph.Nodes[f].In {
  190. if e.Pos() == 0 {
  191. continue
  192. }
  193. call := e.Site
  194. caller := x.funcs[call.Pos()]
  195. if caller != nil {
  196. // TODO: theoretically a format string could be passed to multiple
  197. // arguments of a function. Support this eventually.
  198. continue
  199. }
  200. x.debug(call, "CALL", f.String())
  201. caller = &callData{
  202. call: call,
  203. callee: fd,
  204. formatPos: -1,
  205. argPos: -1,
  206. }
  207. // Offset by one if we are invoking an interface method.
  208. offset := 0
  209. if call.Common().IsInvoke() {
  210. offset = -1
  211. }
  212. x.funcs[call.Pos()] = caller
  213. if fd.argPos >= 0 {
  214. x.visitArgs(caller, call.Common().Args[fd.argPos+offset])
  215. }
  216. x.visitFormats(caller, call.Common().Args[fd.formatPos+offset])
  217. }
  218. }
  219. type posser interface {
  220. Pos() token.Pos
  221. Parent() *ssa.Function
  222. }
  223. func (x *extracter) debug(v posser, header string, args ...interface{}) {
  224. if debug {
  225. pos := ""
  226. if p := v.Parent(); p != nil {
  227. pos = posString(&x.conf, p.Package().Pkg, v.Pos())
  228. }
  229. if header != "CALL" && header != "INSERT" {
  230. header = " " + header
  231. }
  232. fmt.Printf("%-32s%-10s%-15T ", pos+fmt.Sprintf("@%d", v.Pos()), header, v)
  233. for _, a := range args {
  234. fmt.Printf(" %v", a)
  235. }
  236. fmt.Println()
  237. }
  238. }
  239. // visitInit evaluates and collects values assigned to global variables in an
  240. // init function.
  241. func (x *extracter) visitInit(global *ssa.Global, v ssa.Value) {
  242. if v == nil {
  243. return
  244. }
  245. x.debug(v, "GLOBAL", v)
  246. switch v := v.(type) {
  247. case *ssa.Phi:
  248. for _, e := range v.Edges {
  249. x.visitInit(global, e)
  250. }
  251. case *ssa.Const:
  252. // Only record strings with letters.
  253. if str := constant.StringVal(v.Value); isMsg(str) {
  254. cd := x.globalData(global.Pos())
  255. cd.values = append(cd.values, constVal{v.Value, v.Pos()})
  256. }
  257. // TODO: handle %m-directive.
  258. case *ssa.Global:
  259. cd := x.globalData(global.Pos())
  260. cd.others = append(cd.others, v.Pos())
  261. case *ssa.FieldAddr, *ssa.Field:
  262. // TODO: mark field index v.Field of v.X.Type() for extraction. extract
  263. // an example args as to give parameters for the translator.
  264. case *ssa.Slice:
  265. if v.Low == nil && v.High == nil && v.Max == nil {
  266. x.visitInit(global, v.X)
  267. }
  268. case *ssa.Alloc:
  269. if ref := v.Referrers(); ref == nil {
  270. for _, r := range *ref {
  271. values := []ssa.Value{}
  272. for _, o := range r.Operands(nil) {
  273. if o == nil || *o == v {
  274. continue
  275. }
  276. values = append(values, *o)
  277. }
  278. // TODO: return something different if we care about multiple
  279. // values as well.
  280. if len(values) == 1 {
  281. x.visitInit(global, values[0])
  282. }
  283. }
  284. }
  285. case ssa.Instruction:
  286. rands := v.Operands(nil)
  287. if len(rands) == 1 && rands[0] != nil {
  288. x.visitInit(global, *rands[0])
  289. }
  290. }
  291. return
  292. }
  293. // visitFormats finds the original source of the value. The returned index is
  294. // position of the argument if originated from a function argument or -1
  295. // otherwise.
  296. func (x *extracter) visitFormats(call *callData, v ssa.Value) {
  297. if v == nil {
  298. return
  299. }
  300. x.debug(v, "VALUE", v)
  301. switch v := v.(type) {
  302. case *ssa.Phi:
  303. for _, e := range v.Edges {
  304. x.visitFormats(call, e)
  305. }
  306. case *ssa.Const:
  307. // Only record strings with letters.
  308. if isMsg(constant.StringVal(v.Value)) {
  309. x.debug(call.call, "FORMAT", v.Value.ExactString())
  310. call.formats = append(call.formats, v.Value)
  311. }
  312. // TODO: handle %m-directive.
  313. case *ssa.Global:
  314. x.globalData(v.Pos()).call = call
  315. case *ssa.FieldAddr, *ssa.Field:
  316. // TODO: mark field index v.Field of v.X.Type() for extraction. extract
  317. // an example args as to give parameters for the translator.
  318. case *ssa.Slice:
  319. if v.Low == nil && v.High == nil && v.Max == nil {
  320. x.visitFormats(call, v.X)
  321. }
  322. case *ssa.Parameter:
  323. // TODO: handle the function for the index parameter.
  324. f := v.Parent()
  325. for i, p := range f.Params {
  326. if p == v {
  327. if call.formatPos < 0 {
  328. call.formatPos = i
  329. // TODO: is there a better way to detect this is calling
  330. // a method rather than a function?
  331. call.isMethod = len(f.Params) > f.Signature.Params().Len()
  332. x.handleFunc(v.Parent(), call)
  333. } else if debug && i != call.formatPos {
  334. // TODO: support this.
  335. fmt.Printf("WARNING:%s: format string passed to arg %d and %d\n",
  336. posString(&x.conf, call.Pkg(), call.Pos()),
  337. call.formatPos, i)
  338. }
  339. }
  340. }
  341. case *ssa.Alloc:
  342. if ref := v.Referrers(); ref == nil {
  343. for _, r := range *ref {
  344. values := []ssa.Value{}
  345. for _, o := range r.Operands(nil) {
  346. if o == nil || *o == v {
  347. continue
  348. }
  349. values = append(values, *o)
  350. }
  351. // TODO: return something different if we care about multiple
  352. // values as well.
  353. if len(values) == 1 {
  354. x.visitFormats(call, values[0])
  355. }
  356. }
  357. }
  358. // TODO:
  359. // case *ssa.Index:
  360. // // Get all values in the array if applicable
  361. // case *ssa.IndexAddr:
  362. // // Get all values in the slice or *array if applicable.
  363. // case *ssa.Lookup:
  364. // // Get all values in the map if applicable.
  365. case *ssa.FreeVar:
  366. // TODO: find the link between free variables and parameters:
  367. //
  368. // func freeVar(p *message.Printer, str string) {
  369. // fn := func(p *message.Printer) {
  370. // p.Printf(str)
  371. // }
  372. // fn(p)
  373. // }
  374. case *ssa.Call:
  375. case ssa.Instruction:
  376. rands := v.Operands(nil)
  377. if len(rands) == 1 && rands[0] != nil {
  378. x.visitFormats(call, *rands[0])
  379. }
  380. }
  381. }
  382. // Note: a function may have an argument marked as both format and passthrough.
  383. // visitArgs collects information on arguments. For wrapped functions it will
  384. // just determine the position of the variable args slice.
  385. func (x *extracter) visitArgs(fd *callData, v ssa.Value) {
  386. if v == nil {
  387. return
  388. }
  389. x.debug(v, "ARGV", v)
  390. switch v := v.(type) {
  391. case *ssa.Slice:
  392. if v.Low == nil && v.High == nil && v.Max == nil {
  393. x.visitArgs(fd, v.X)
  394. }
  395. case *ssa.Parameter:
  396. // TODO: handle the function for the index parameter.
  397. f := v.Parent()
  398. for i, p := range f.Params {
  399. if p == v {
  400. fd.argPos = i
  401. }
  402. }
  403. case *ssa.Alloc:
  404. if ref := v.Referrers(); ref == nil {
  405. for _, r := range *ref {
  406. values := []ssa.Value{}
  407. for _, o := range r.Operands(nil) {
  408. if o == nil || *o == v {
  409. continue
  410. }
  411. values = append(values, *o)
  412. }
  413. // TODO: return something different if we care about
  414. // multiple values as well.
  415. if len(values) == 1 {
  416. x.visitArgs(fd, values[0])
  417. }
  418. }
  419. }
  420. case ssa.Instruction:
  421. rands := v.Operands(nil)
  422. if len(rands) == 1 && rands[0] != nil {
  423. x.visitArgs(fd, *rands[0])
  424. }
  425. }
  426. }
  427. // print returns Go syntax for the specified node.
  428. func (x *extracter) print(n ast.Node) string {
  429. var buf bytes.Buffer
  430. format.Node(&buf, x.conf.Fset, n)
  431. return buf.String()
  432. }
  433. type packageExtracter struct {
  434. f *ast.File
  435. x *extracter
  436. info *loader.PackageInfo
  437. cmap ast.CommentMap
  438. }
  439. func (px packageExtracter) getComment(n ast.Node) string {
  440. cs := px.cmap.Filter(n).Comments()
  441. if len(cs) > 0 {
  442. return strings.TrimSpace(cs[0].Text())
  443. }
  444. return ""
  445. }
  446. func (x *extracter) extractMessages() {
  447. prog := x.iprog
  448. files := []packageExtracter{}
  449. for _, info := range x.iprog.AllPackages {
  450. for _, f := range info.Files {
  451. // Associate comments with nodes.
  452. px := packageExtracter{
  453. f, x, info,
  454. ast.NewCommentMap(prog.Fset, f, f.Comments),
  455. }
  456. files = append(files, px)
  457. }
  458. }
  459. for _, px := range files {
  460. ast.Inspect(px.f, func(n ast.Node) bool {
  461. switch v := n.(type) {
  462. case *ast.CallExpr:
  463. if d := x.funcs[v.Lparen]; d != nil {
  464. d.expr = v
  465. }
  466. }
  467. return true
  468. })
  469. }
  470. for _, px := range files {
  471. ast.Inspect(px.f, func(n ast.Node) bool {
  472. switch v := n.(type) {
  473. case *ast.CallExpr:
  474. return px.handleCall(v)
  475. case *ast.ValueSpec:
  476. return px.handleGlobal(v)
  477. }
  478. return true
  479. })
  480. }
  481. }
  482. func (px packageExtracter) handleGlobal(spec *ast.ValueSpec) bool {
  483. comment := px.getComment(spec)
  484. for _, ident := range spec.Names {
  485. data, ok := px.x.globals[ident.Pos()]
  486. if !ok {
  487. continue
  488. }
  489. name := ident.Name
  490. var arguments []argument
  491. if data.call != nil {
  492. arguments = px.getArguments(data.call)
  493. } else if !strings.HasPrefix(name, "msg") && !strings.HasPrefix(name, "Msg") {
  494. continue
  495. }
  496. data.visit(px.x, func(c constant.Value) {
  497. px.addMessage(spec.Pos(), []string{name}, c, comment, arguments)
  498. })
  499. }
  500. return true
  501. }
  502. func (px packageExtracter) handleCall(call *ast.CallExpr) bool {
  503. x := px.x
  504. data := x.funcs[call.Lparen]
  505. if data == nil || len(data.formats) == 0 {
  506. return true
  507. }
  508. if data.expr != call {
  509. panic("invariant `data.call != call` failed")
  510. }
  511. x.debug(data.call, "INSERT", data.formats)
  512. argn := data.callFormatPos()
  513. if argn >= len(call.Args) {
  514. return true
  515. }
  516. format := call.Args[argn]
  517. arguments := px.getArguments(data)
  518. comment := ""
  519. key := []string{}
  520. if ident, ok := format.(*ast.Ident); ok {
  521. key = append(key, ident.Name)
  522. if v, ok := ident.Obj.Decl.(*ast.ValueSpec); ok && v.Comment != nil {
  523. // TODO: get comment above ValueSpec as well
  524. comment = v.Comment.Text()
  525. }
  526. }
  527. if c := px.getComment(call.Args[0]); c != "" {
  528. comment = c
  529. }
  530. formats := data.formats
  531. for _, c := range formats {
  532. px.addMessage(call.Lparen, key, c, comment, arguments)
  533. }
  534. return true
  535. }
  536. func (px packageExtracter) getArguments(data *callData) []argument {
  537. arguments := []argument{}
  538. x := px.x
  539. info := px.info
  540. if data.callArgsStart() >= 0 {
  541. args := data.expr.Args[data.callArgsStart():]
  542. for i, arg := range args {
  543. expr := x.print(arg)
  544. val := ""
  545. if v := info.Types[arg].Value; v != nil {
  546. val = v.ExactString()
  547. switch arg.(type) {
  548. case *ast.BinaryExpr, *ast.UnaryExpr:
  549. expr = val
  550. }
  551. }
  552. arguments = append(arguments, argument{
  553. ArgNum: i + 1,
  554. Type: info.Types[arg].Type.String(),
  555. UnderlyingType: info.Types[arg].Type.Underlying().String(),
  556. Expr: expr,
  557. Value: val,
  558. Comment: px.getComment(arg),
  559. Position: posString(&x.conf, info.Pkg, arg.Pos()),
  560. // TODO report whether it implements
  561. // interfaces plural.Interface,
  562. // gender.Interface.
  563. })
  564. }
  565. }
  566. return arguments
  567. }
  568. func (px packageExtracter) addMessage(
  569. pos token.Pos,
  570. key []string,
  571. c constant.Value,
  572. comment string,
  573. arguments []argument) {
  574. x := px.x
  575. fmtMsg := constant.StringVal(c)
  576. ph := placeholders{index: map[string]string{}}
  577. trimmed, _, _ := trimWS(fmtMsg)
  578. p := fmtparser.Parser{}
  579. simArgs := make([]interface{}, len(arguments))
  580. for i, v := range arguments {
  581. simArgs[i] = v
  582. }
  583. msg := ""
  584. p.Reset(simArgs)
  585. for p.SetFormat(trimmed); p.Scan(); {
  586. name := ""
  587. var arg *argument
  588. switch p.Status {
  589. case fmtparser.StatusText:
  590. msg += p.Text()
  591. continue
  592. case fmtparser.StatusSubstitution,
  593. fmtparser.StatusBadWidthSubstitution,
  594. fmtparser.StatusBadPrecSubstitution:
  595. arguments[p.ArgNum-1].used = true
  596. arg = &arguments[p.ArgNum-1]
  597. name = getID(arg)
  598. case fmtparser.StatusBadArgNum, fmtparser.StatusMissingArg:
  599. arg = &argument{
  600. ArgNum: p.ArgNum,
  601. Position: posString(&x.conf, px.info.Pkg, pos),
  602. }
  603. name, arg.UnderlyingType = verbToPlaceholder(p.Text(), p.ArgNum)
  604. }
  605. sub := p.Text()
  606. if !p.HasIndex {
  607. r, sz := utf8.DecodeLastRuneInString(sub)
  608. sub = fmt.Sprintf("%s[%d]%c", sub[:len(sub)-sz], p.ArgNum, r)
  609. }
  610. msg += fmt.Sprintf("{%s}", ph.addArg(arg, name, sub))
  611. }
  612. key = append(key, msg)
  613. // Add additional Placeholders that can be used in translations
  614. // that are not present in the string.
  615. for _, arg := range arguments {
  616. if arg.used {
  617. continue
  618. }
  619. ph.addArg(&arg, getID(&arg), fmt.Sprintf("%%[%d]v", arg.ArgNum))
  620. }
  621. x.messages = append(x.messages, Message{
  622. ID: key,
  623. Key: fmtMsg,
  624. Message: Text{Msg: msg},
  625. // TODO(fix): this doesn't get the before comment.
  626. Comment: comment,
  627. Placeholders: ph.slice,
  628. Position: posString(&x.conf, px.info.Pkg, pos),
  629. })
  630. }
  631. func posString(conf *loader.Config, pkg *types.Package, pos token.Pos) string {
  632. p := conf.Fset.Position(pos)
  633. file := fmt.Sprintf("%s:%d:%d", filepath.Base(p.Filename), p.Line, p.Column)
  634. return filepath.Join(pkg.Path(), file)
  635. }
  636. func getID(arg *argument) string {
  637. s := getLastComponent(arg.Expr)
  638. s = strip(s)
  639. s = strings.Replace(s, " ", "", -1)
  640. // For small variable names, use user-defined types for more info.
  641. if len(s) <= 2 && arg.UnderlyingType != arg.Type {
  642. s = getLastComponent(arg.Type)
  643. }
  644. return strings.Title(s)
  645. }
  646. // strip is a dirty hack to convert function calls to placeholder IDs.
  647. func strip(s string) string {
  648. s = strings.Map(func(r rune) rune {
  649. if unicode.IsSpace(r) || r == '-' {
  650. return '_'
  651. }
  652. if !unicode.In(r, unicode.Letter, unicode.Mark, unicode.Number) {
  653. return -1
  654. }
  655. return r
  656. }, s)
  657. // Strip "Get" from getter functions.
  658. if strings.HasPrefix(s, "Get") || strings.HasPrefix(s, "get") {
  659. if len(s) > len("get") {
  660. r, _ := utf8.DecodeRuneInString(s)
  661. if !unicode.In(r, unicode.Ll, unicode.M) { // not lower or mark
  662. s = s[len("get"):]
  663. }
  664. }
  665. }
  666. return s
  667. }
  668. // verbToPlaceholder gives a name for a placeholder based on the substitution
  669. // verb. This is only to be used if there is otherwise no other type information
  670. // available.
  671. func verbToPlaceholder(sub string, pos int) (name, underlying string) {
  672. r, _ := utf8.DecodeLastRuneInString(sub)
  673. name = fmt.Sprintf("Arg_%d", pos)
  674. switch r {
  675. case 's', 'q':
  676. underlying = "string"
  677. case 'd':
  678. name = "Integer"
  679. underlying = "int"
  680. case 'e', 'f', 'g':
  681. name = "Number"
  682. underlying = "float64"
  683. case 'm':
  684. name = "Message"
  685. underlying = "string"
  686. default:
  687. underlying = "interface{}"
  688. }
  689. return name, underlying
  690. }
  691. type placeholders struct {
  692. index map[string]string
  693. slice []Placeholder
  694. }
  695. func (p *placeholders) addArg(arg *argument, name, sub string) (id string) {
  696. id = name
  697. alt, ok := p.index[id]
  698. for i := 1; ok && alt != sub; i++ {
  699. id = fmt.Sprintf("%s_%d", name, i)
  700. alt, ok = p.index[id]
  701. }
  702. p.index[id] = sub
  703. p.slice = append(p.slice, Placeholder{
  704. ID: id,
  705. String: sub,
  706. Type: arg.Type,
  707. UnderlyingType: arg.UnderlyingType,
  708. ArgNum: arg.ArgNum,
  709. Expr: arg.Expr,
  710. Comment: arg.Comment,
  711. })
  712. return id
  713. }
  714. func getLastComponent(s string) string {
  715. return s[1+strings.LastIndexByte(s, '.'):]
  716. }
  717. // isMsg returns whether s should be translated.
  718. func isMsg(s string) bool {
  719. // TODO: parse as format string and omit strings that contain letters
  720. // coming from format verbs.
  721. for _, r := range s {
  722. if unicode.In(r, unicode.L) {
  723. return true
  724. }
  725. }
  726. return false
  727. }