-
Notifications
You must be signed in to change notification settings - Fork 91
feat: support localization (#309) #861
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
base: dev
Are you sure you want to change the base?
Changes from 15 commits
d06afec
3cb34cb
92928ed
9e8d93f
529e197
d7ab57b
27dee83
aaa5ec2
bdee7ad
e939838
8ac690a
95f0f2c
5c0aa19
c292a71
4915cc8
5cbdfe9
9a3a116
696b6a5
67b15ad
68827d5
835afe2
2abc221
ab30724
861923e
509a9ce
e9136b8
20f8d4d
74e51e7
b79d59a
aa21c67
9dac59b
95c9f32
a51ca43
7e1151e
74a5553
3526de2
da69a4d
4234b04
5c09b42
9f7984e
2118b5e
31b82c4
7de7b32
60058b8
bba85aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,11 @@ jobs: | |
| with: | ||
| submodules: recursive | ||
|
|
||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.13" | ||
| cache: "pip" | ||
|
|
||
| - uses: ilammy/[email protected] | ||
|
|
||
| - uses: lukka/[email protected] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ build/ | |
| bin/ | ||
| dist/ | ||
| .vs*/ | ||
| .idea/ | ||
| CMakeUserPresets.json | ||
| shadertoolsconfig.json | ||
| .vscode | ||
| .vscode | ||
| fonts/ | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #include "Translate.h" | ||
|
|
||
| #include "SKSE/Translation.h" | ||
|
|
||
| namespace Util | ||
| { | ||
| std::string Translate(const std::string& key) | ||
| { | ||
| std::string buffer; | ||
|
|
||
| if (SKSE::Translation::Translate(key, buffer)) { | ||
| return buffer; | ||
| } | ||
|
|
||
| return key; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| #pragma once | ||
|
|
||
| #include <fmt/core.h> | ||
| #include <string> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| namespace Util | ||
| { | ||
| std::string Translate(const std::string& key); | ||
|
|
||
| struct Translatable | ||
| { | ||
| std::string_view key; | ||
|
|
||
| template <typename... Args> | ||
| std::string operator()(Args&&... args) const | ||
| { | ||
| auto [keyArgs, otherArgs] = filterArgs(std::forward<Args>(args)...); | ||
| std::string preprocessedKey = preprocessKey(key, keyArgs); | ||
| std::string translatedKey = Translate(preprocessedKey); | ||
| return formatWithArgs(translatedKey, otherArgs); | ||
| } | ||
|
|
||
| explicit operator std::string() const noexcept | ||
| { | ||
| return Translate(std::string(key)); | ||
| } | ||
|
|
||
| private: | ||
| template <typename... Args> | ||
| static std::pair<std::vector<std::string_view>, std::vector<std::string>> filterArgs(Args&&... args) | ||
| { | ||
| std::vector<std::string_view> keyArgs; | ||
| std::vector<std::string> otherArgs; | ||
|
|
||
| auto processArg = [&](auto&& arg) { | ||
| if constexpr (std::is_convertible_v<decltype(arg), std::string_view>) { | ||
| if (std::string_view argView(arg); argView.starts_with('$')) | ||
| keyArgs.emplace_back(argView); | ||
| else | ||
| otherArgs.emplace_back(argView); | ||
| } else { | ||
| otherArgs.emplace_back(fmt::format("{}", arg)); | ||
| } | ||
| }; | ||
|
|
||
| (processArg(std::forward<Args>(args)), ...); | ||
| return { std::move(keyArgs), std::move(otherArgs) }; | ||
| } | ||
|
|
||
| static std::string preprocessKey(const std::string_view key, const std::vector<std::string_view>& keyArgs) | ||
| { | ||
| std::string result(key); | ||
| size_t pos = 0; | ||
|
|
||
| for (const auto& arg : keyArgs) { | ||
| if ((pos = result.find("{}", pos)) != std::string::npos) { | ||
| result.replace(pos, 2, "{" + std::string(arg) + "}"); | ||
| pos += arg.size() + 2; | ||
| } else | ||
| break; | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| static std::string formatWithArgs(const std::string& translation, const std::vector<std::string>& args) | ||
| { | ||
| std::vector<fmt::format_context::format_arg> formatArgs; | ||
| for (const auto& arg : args) { | ||
| formatArgs.push_back(fmt::detail::make_arg<fmt::format_context>(arg)); | ||
| } | ||
|
|
||
| return vformat(translation, fmt::basic_format_args(formatArgs.data(), static_cast<int>(formatArgs.size()))); | ||
| } | ||
| }; | ||
|
Comment on lines
+68
to
+77
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid using fmt internal APIs. The code uses Use fmt's public API instead: static std::string formatWithArgs(const std::string& translation, const std::vector<std::string>& args)
{
- std::vector<fmt::format_context::format_arg> formatArgs;
- for (const auto& arg : args) {
- formatArgs.push_back(fmt::detail::make_arg<fmt::format_context>(arg));
- }
-
- return vformat(translation, fmt::basic_format_args(formatArgs.data(), static_cast<int>(formatArgs.size())));
+ return std::apply([&translation](const auto&... args) {
+ return fmt::vformat(translation, fmt::make_format_args(args...));
+ }, std::tuple_cat(std::make_tuple(), args));
}Alternatively, if variadic expansion isn't suitable, consider using fmt::dynamic_format_arg_store which is part of the public API.
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| inline Util::Translatable operator"" _i18n(const char* key, std::size_t) noexcept | ||
| { | ||
| return Util::Translatable{ std::string_view(key) }; | ||
| } | ||
|
|
||
| inline const char* operator"" _i18n_cs(const char* key, std::size_t) noexcept | ||
| { | ||
| thread_local std::string translation; | ||
| translation = Util::Translate(std::string(key)); | ||
| return translation.c_str(); | ||
| } | ||
|
Comment on lines
+85
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thread-local storage may cause issues with multiple calls in the same expression. The Example of the issue: // Both pointers will point to the same string (the last translation)
printf("%s %s", "key1"_i18n_cs, "key2"_i18n_cs);Consider using a thread-local circular buffer or documenting this limitation clearly: inline const char* operator"" _i18n_cs(const char* key, std::size_t) noexcept
{
- thread_local std::string translation;
- translation = Util::Translate(std::string(key));
- return translation.c_str();
+ thread_local std::array<std::string, 4> translations;
+ thread_local size_t index = 0;
+ translations[index] = Util::Translate(std::string(key));
+ const char* result = translations[index].c_str();
+ index = (index + 1) % translations.size();
+ return result;
}🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| #!/usr/bin/env python3 | ||
| import argparse | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
| from urllib.parse import urlparse | ||
| from urllib.request import urlretrieve | ||
|
|
||
| try: | ||
| from fontTools import subset | ||
| except ImportError: | ||
| print("fonttools not found. Installing via pip...") | ||
| subprocess.check_call([sys.executable, "-m", "pip", "install", "fonttools"]) | ||
| from fontTools import subset | ||
|
|
||
| def parse_arguments(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Automate collecting unique characters and generate font subset." | ||
| ) | ||
| parser.add_argument( | ||
| "--input", | ||
| type=str, | ||
| default="https://github.com/adobe-fonts/source-han-sans/raw/release/Variable/OTC/SourceHanSans-VF.ttf.ttc", | ||
| help="Path or URL to the original font file (e.g., C:/Windows/Fonts/msyh.ttc)." | ||
| ) | ||
| parser.add_argument( | ||
| "--output", | ||
| type=str, | ||
| default="package/Interface/CommunityShaders/Fonts/CommunityShaders.ttf", | ||
| help="Path for the output subset font file (e.g., SubsetFont.ttf)." | ||
| ) | ||
| parser.add_argument( | ||
| "--text_dir", | ||
| type=str, | ||
| default="package/Interface/Translations", | ||
| help="Directory to scan for translated files." | ||
| ) | ||
| parser.add_argument( | ||
| "--font_number", | ||
| type=int, | ||
| default=0, | ||
| help="Font number of the font file (default: 0)." | ||
| ) | ||
| return parser.parse_args() | ||
|
|
||
| def is_url(path): | ||
| try: | ||
| result = urlparse(path) | ||
| return all([result.scheme, result.netloc]) | ||
| except ValueError: | ||
| return False | ||
|
|
||
| def get_cache_dir(): | ||
| cache_path = Path.cwd() / "fonts" | ||
| cache_path.mkdir(parents=True, exist_ok=True) | ||
| return cache_path | ||
|
|
||
| def download_font(url): | ||
| download_path = get_cache_dir() / Path(url).name | ||
| if not download_path.is_file(): | ||
| print(f"Downloading font from '{url}' to '{download_path}'...") | ||
| try: | ||
| urlretrieve(url, download_path) | ||
| except Exception as e: | ||
| sys.exit(f"Error downloading font: {e}") | ||
| return str(download_path) | ||
|
|
||
| def collect_unique_chars(input_dir, extension=".txt"): | ||
| unique_chars = set() | ||
| for file in Path(input_dir).rglob(f"*{extension}"): | ||
| try: | ||
| with file.open('r', encoding='utf-16-le', errors='ignore') as f: | ||
| unique_chars.update(c for c in f.read() if c.isprintable()) | ||
| except Exception as e: | ||
| print(f"Error reading '{file}': {e}", file=sys.stderr) | ||
| return unique_chars | ||
|
|
||
| def subset_font(original_path, subset_path, text, font_number): | ||
| try: | ||
| options = subset.Options(font_number = font_number) | ||
| subsetter = subset.Subsetter(options=options) | ||
| subsetter.populate(text=text) | ||
|
|
||
| with subset.load_font(str(original_path), options) as font: | ||
| subsetter.subset(font) | ||
| subset.save_font(font, str(subset_path), options) | ||
| print(f"Subset font saved to '{subset_path}'.") | ||
| except Exception as e: | ||
| sys.exit(f"Error during font subsetting: {e}") | ||
|
|
||
| def main(): | ||
| args = parse_arguments() | ||
|
|
||
| original_font = download_font(args.input) if is_url(args.input) else args.input | ||
| if not Path(original_font).is_file(): | ||
| sys.exit(f"Error: Font file '{original_font}' does not exist.") | ||
|
|
||
| unique_chars = collect_unique_chars(args.text_dir) | ||
| if not unique_chars: | ||
| sys.exit("No characters found. Exiting.") | ||
|
|
||
| basic_latin = {chr(c) for c in range(0x0020, 0x0100)} | ||
| combined_chars = unique_chars | basic_latin | ||
|
|
||
| print(f"Total characters to include in subset: {len(combined_chars)}.") | ||
|
|
||
| subset_font(original_font, args.output, ''.join(combined_chars), args.font_number) | ||
|
|
||
| if __name__ == "__main__": | ||
| main() |
Uh oh!
There was an error while loading. Please reload this page.