Skip to content

Fluent-to-PO / PO-to-Fluent conversion actions #650

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

Draft
wants to merge 11 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _None_

### New Features

_None_
- Introduce `fluent_to_po_action` and `po_to_fluent_action` for bidirectional conversion between Fluent (.ftl) and PO (.po) localization files to facilitate GlotPress integration workflows. [#650]

### Bug Fixes

Expand Down
36 changes: 33 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ PATH
chroma (= 0.2.0)
diffy (~> 3.3)
fastlane (~> 2.213)
gettext (~> 3.4)
git (~> 1.3)
google-cloud-storage (~> 1.31)
java-properties (~> 0.3.0)
Expand All @@ -16,7 +17,11 @@ PATH
plist (~> 3.1)
progress_bar (~> 1.3)
rake (>= 12.3, < 14.0)
rake-compiler (~> 1.0)
rake-compiler-dock (~> 1.0)
rqrcode (~> 2.0)
rubocop (~> 1.50)
ruby-progressbar (~> 1.11)
twitter_cldr (~> 6.11)
xcodeproj (~> 1.22)

GEM
Expand Down Expand Up @@ -70,12 +75,15 @@ GEM
buildkit (1.6.1)
sawyer (>= 0.6)
buildkite-test_collector (2.9.0)
camertron-eprun (1.1.1)
chroma (0.2.0)
chunky_png (1.4.0)
claide (1.1.0)
claide-plugins (0.9.2)
cork
nap
open4 (~> 1.3)
cldr-plurals-runtime-rb (1.1.0)
cocoapods (1.16.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
Expand Down Expand Up @@ -161,6 +169,7 @@ GEM
dotenv (2.8.1)
drb (2.2.1)
emoji_regex (3.2.3)
erubi (1.13.1)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
Expand Down Expand Up @@ -241,8 +250,15 @@ GEM
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.17.1)
forwardable (1.3.3)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gettext (3.5.1)
erubi
locale (>= 2.0.5)
prime
racc
text (>= 1.3.0)
gh_inspector (1.1.3)
git (1.19.1)
addressable (~> 2.8)
Expand Down Expand Up @@ -302,6 +318,7 @@ GEM
kramdown (~> 2.0)
language_server-protocol (3.17.0.4)
lint_roller (1.1.0)
locale (2.1.4)
logger (1.6.6)
method_source (0.9.2)
mini_magick (4.13.2)
Expand Down Expand Up @@ -334,6 +351,9 @@ GEM
racc
pkg-config (1.6.0)
plist (3.7.2)
prime (0.1.3)
forwardable
singleton
progress_bar (1.3.4)
highline (>= 1.6)
options (~> 2.3.0)
Expand All @@ -345,8 +365,7 @@ GEM
racc (1.8.1)
rainbow (3.1.1)
rake (13.2.1)
rake-compiler (1.2.9)
rake
rake-compiler-dock (1.9.1)
rchardet (1.9.0)
regexp_parser (2.10.0)
representable (3.2.0)
Expand All @@ -359,6 +378,10 @@ GEM
observer (~> 0.1)
pkg-config (~> 1.4)
rouge (3.28.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
Expand Down Expand Up @@ -411,15 +434,22 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
singleton (0.3.0)
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
text (1.3.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
twitter_cldr (6.14.0)
base64
camertron-eprun
cldr-plurals-runtime-rb (~> 1.1)
tzinfo
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
Expand Down
9 changes: 7 additions & 2 deletions fastlane-plugin-wpmreleasetoolkit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'fastlane/plugin/wpmreleasetoolkit/version'

Gem::Specification.new do |spec|
spec.name = 'fastlane-plugin-wpmreleasetoolkit'
spec.name = Fastlane::Wpmreleasetoolkit::NAME
spec.version = Fastlane::Wpmreleasetoolkit::VERSION
spec.author = 'Automattic'
spec.email = 'mobile@automattic.com'
Expand All @@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'chroma', '0.2.0'
spec.add_dependency 'diffy', '~> 3.3'
spec.add_dependency 'fastlane', '~> 2.213'
spec.add_dependency 'gettext', '~> 3.4'
spec.add_dependency 'git', '~> 1.3'
spec.add_dependency 'java-properties', '~> 0.3.0'
spec.add_dependency 'nokogiri', '~> 1.11'
Expand All @@ -39,7 +40,11 @@ Gem::Specification.new do |spec|
spec.add_dependency 'plist', '~> 3.1'
spec.add_dependency 'progress_bar', '~> 1.3'
spec.add_dependency 'rake', '>= 12.3', '< 14.0'
spec.add_dependency 'rake-compiler', '~> 1.0'
spec.add_dependency 'rake-compiler-dock', '~> 1.0'
spec.add_dependency 'rqrcode', '~> 2.0'
spec.add_dependency 'rubocop', '~> 1.50'
spec.add_dependency 'ruby-progressbar', '~> 1.11'
spec.add_dependency 'twitter_cldr', '~> 6.11'
spec.add_dependency 'xcodeproj', '~> 1.22'

# `google-cloud-storage` is required by fastlane, but we pin it in case it's not in the future
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

require 'gettext'
require 'gettext/po_parser'
require 'gettext/po'
require 'twitter_cldr'

module Fastlane
module Actions
class FluentToPoAction < Action
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: While it's nice to have concise name for actions so they don't become a mouthful, I feel like we could use a bit more descriptive action names—especially so that their goal is not too mysterious when we see those action names without much context attached, like when called in a Fastfile or when running fastlane actions to list all the available actions…

Maybe convert_fluent_translation_file_to_po_format / convert_po_translation_file_to_fluent_format, or convert_strings_file_fluent_to_po / convert_strings_file_po_to_fluent? Sure, that's a bit more verbose to type, but also less ambiguous—especially for people who never heard of Fluent and that file format and would see that action in the while without knowing that Fluent and Po are terms related to translations and strings file formats?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed 👍 I also like the side effect of grouping similar actions by name (convert_*).
What about convert_fluent_strings_file_to_po vs. convert_po_strings_file_to_fluent? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, I like that naming suggestion

def self.run(params)
require_relative '../../helper/fluent_localization_helper'

UI.message('Converting Fluent file to PO format...')

input_path = File.expand_path(params[:input_file])
output_path = File.expand_path(params[:output_file])

UI.user_error!("Input file does not exist: #{input_path}") unless File.exist?(input_path)

# Parse the Fluent file
fluent_entries = Helper::FluentLocalizationHelper.parse_fluent_file(input_path)
UI.message("Found #{fluent_entries.length} entries in Fluent file")

# Convert to PO format
po_content = Helper::FluentLocalizationHelper.fluent_to_po(
fluent_file: File.basename(input_path),
fluent_entries: fluent_entries,
locale: params[:locale],
project_name: params[:project_name],
project_version: params[:project_version]
)

File.write(output_path, po_content, encoding: 'utf-8')

UI.success("Successfully converted Fluent file to PO: #{output_path}")

output_path
end

def self.description
'Convert Fluent (.ftl) files to PO (.po) format for GlotPress integration'
end

def self.authors
['Automattic']
end

def self.available_options
[
FastlaneCore::ConfigItem.new(
key: :input_file,
description: 'Path to the input Fluent (.ftl) file',
optional: false,
type: String
),
FastlaneCore::ConfigItem.new(
key: :output_file,
description: 'Path to the output PO (.po) file',
optional: false,
type: String
),
FastlaneCore::ConfigItem.new(
key: :locale,
description: 'Target locale (e.g., en-US, es-ES)',
optional: false,
type: String
),
FastlaneCore::ConfigItem.new(
key: :project_name,
description: 'Project name for the PO header',
optional: true,
type: String,
default_value: ''
),
FastlaneCore::ConfigItem.new(
key: :project_version,
description: 'Project version for the PO header',
optional: true,
type: String,
default_value: ''
),
]
end

def self.is_supported?(platform)
true
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require 'gettext'
require 'gettext/po_parser'
require 'gettext/po'

module Fastlane
module Actions
class PoToFluentAction < Action
def self.run(params)
require_relative '../../helper/fluent_localization_helper'

UI.message('Converting PO file to Fluent format...')

input_path = File.expand_path(params[:input_file])
output_path = File.expand_path(params[:output_file])

UI.user_error!("Input file does not exist: #{input_path}") unless File.exist?(input_path)

# Parse the PO file
po_entries = Helper::FluentLocalizationHelper.parse_po_file(input_path)

# Convert to Fluent format
fluent_content = Helper::FluentLocalizationHelper.po_to_fluent(po_entries)

if !params[:allow_empty_file] && fluent_content.strip.empty?
UI.message('No translated content found in PO file')
return
end

File.write(output_path, fluent_content, encoding: 'utf-8')
UI.success("Successfully converted PO file to Fluent: #{output_path}")

output_path
end

def self.description
'Convert PO (.po) files to Fluent (.ftl) format after translation'
end

def self.authors
['Automattic']
end

def self.available_options
[
FastlaneCore::ConfigItem.new(
key: :input_file,
description: 'Path to the input PO (.po) file',
optional: false,
type: String
),
Comment on lines +47 to +52
Copy link
Contributor

@AliSoftware AliSoftware Jun 4, 2025

Choose a reason for hiding this comment

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

💡 What about using fastlane's built-in verify_block: to check that the input file exists, and then remove that existence test on line 18?

FastlaneCore::ConfigItem.new(
key: :output_file,
description: 'Path to the output Fluent (.ftl) file',
optional: false,
type: String
),
FastlaneCore::ConfigItem.new(
key: :allow_empty_file,
description: 'Whether to allow an empty file when no translated content is found',
optional: true,
type: Boolean,
default_value: false
),
]
end

def self.is_supported?(platform)
true
end
end
end
end
Loading